From 133aed9c851ec4517f29931156bd6cdf3f8f034f Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 14 Jan 2018 20:28:19 -0600 Subject: [PATCH 01/24] =?UTF-8?q?Tablas=20para=20N=C3=B3mina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/controllers/util.py | 12 + source/app/models/main.py | 558 ++++++++- source/app/settings.py | 8 +- source/db/valores_iniciales.json | 1852 ++++++++++++++++++++++++++++++ 4 files changed, 2421 insertions(+), 9 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 38bb216..8ac98bb 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -260,6 +260,14 @@ def get_file(path): return open(path, 'rb') +def get_files(path, ext='xml'): + docs = [] + for folder, _, files in os.walk(path): + pattern = re.compile('\.{}'.format(ext), re.IGNORECASE) + docs += [os.path.join(folder,f) for f in files if pattern.search(f)] + return tuple(docs) + + def read_file(path, mode='rb'): return open(path, mode).read() @@ -308,6 +316,10 @@ def loads(data): return json.loads(data) +def import_json(path): + return loads(read_file(path, 'r')) + + def clean(values): for k, v in values.items(): if isinstance(v, str): diff --git a/source/app/models/main.py b/source/app/models/main.py index 74d6bbd..1699698 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -17,7 +17,8 @@ if __name__ == '__main__': from controllers import util from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \ INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \ - CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, DECIMALES_TAX, TITLE_APP + CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \ + DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP FORMAT = '{0:.2f}' @@ -650,6 +651,7 @@ class SATRegimenes(BaseModel): class Emisor(BaseModel): rfc = TextField(unique=True) + curp = TextField(unique=True) nombre = TextField(default='') nombre_comercial = TextField(default='') calle = TextField(default='') @@ -675,6 +677,7 @@ class Emisor(BaseModel): token_timbrado = TextField(default='') token_soporte = TextField(default='') logo = TextField(default='') + registro_patronal = TextField(default='') regimenes = ManyToManyField(SATRegimenes, related_name='emisores') def __str__(self): @@ -1843,6 +1846,208 @@ class SATUsoCfdi(BaseModel): return tuple(rows) +class SATEstados(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + pais = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name', 'pais'), True), + ) + + def __str__(self): + return 'Estado: {} ({})'.format(self.name, self.key) + + +class SATOrigenRecurso(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Origen Recurso: {} ({})'.format(self.name, self.key) + + +class SATPeriodicidadPago(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Periodicidad de Pago: {} ({})'.format(self.name, self.key) + + +class SATTipoContrato(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Contrato: {} ({})'.format(self.name, self.key) + + +class SATTipoDeduccion(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Deducción: {} ({})'.format(self.name, self.key) + + +class SATTipoHoras(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Horas: {} ({})'.format(self.name, self.key) + + +class SATTipoIncapacidad(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Incapacidad: {} ({})'.format(self.name, self.key) + + +class SATTipoJornada(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Jornada: {} ({})'.format(self.name, self.key) + + +class SATTipoNomina(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Nómina: {} ({})'.format(self.name, self.key) + + +class SATTipoOtroPago(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Otro Pago: {} ({})'.format(self.name, self.key) + + +class SATTipoPercepcion(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + clave = TextField(default='') + nombre = TextField(default='') + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de Percepción: {} ({})'.format(self.name, self.key) + + +class SATTipoRegimen(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Regimen de contratación: {} ({})'.format(self.name, self.key) + + +class SATRiesgoPuesto(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=True) + + class Meta: + order_by = ('name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Riesgo Puesto: {} ({})'.format(self.name, self.key) + + class TipoCambio(BaseModel): dia = DateField(default=util.now) moneda = ForeignKeyField(SATMonedas) @@ -4975,6 +5180,275 @@ class SeriesProductos(BaseModel): order_by = ('serie',) +class Departamentos(BaseModel): + nombre = TextField(default='') + descripcion = TextField(default='') + + class Meta: + order_by = ('nombre',) + + +class Puestos(BaseModel): + nombre = TextField(default='') + descripcion = TextField(default='') + departamento = ForeignKeyField(Departamentos) + + class Meta: + order_by = ('nombre',) + indexes = ( + (('nombre', 'departamento'), True), + ) + + +class Empleados(BaseModel): + num_empleado = TextField(default='') + rfc = TextField(default='', unique=True) + curp = TextField(default='', unique=True) + nombre = TextField(default='') + paterno = TextField(default='') + materno = TextField(default='') + nombre_completo = TextField(default='') + es_activo = BooleanField(default=True) + es_extranjero = BooleanField(default=False) + fecha_alta = DateField(default=util.now) + fecha_ingreso = DateField(default=util.now) + imss = TextField(default='') + tipo_contrato = ForeignKeyField(SATTipoContrato) + es_sindicalizado = BooleanField(default=False) + tipo_jornada = ForeignKeyField(SATTipoJornada, null=True) + tipo_regimen = ForeignKeyField(SATTipoRegimen) + puesto = ForeignKeyField(Puestos, null=True) + riesgo_puesto = ForeignKeyField(SATRiesgoPuesto, null=True) + periodicidad_pago = ForeignKeyField(SATPeriodicidadPago) + banco = ForeignKeyField(SATBancos, null=True) + cuenta_bancaria = TextField(default='') + clabe = TextField(default='') + salario_base = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + salario_diario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + estado = ForeignKeyField(SATEstados) + codigo_postal = TextField(default='') + notas = TextField(default='') + + class Meta: + order_by = ('nombre_completo',) + indexes = ( + (('num_empleado', 'rfc'), True), + ) + + +class CfdiNomina(BaseModel): + empleado = ForeignKeyField(Empleados) + version = TextField(default=CURRENT_CFDI) + serie = TextField(default='') + folio = IntegerField(default=0) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) + fecha_timbrado = DateTimeField(null=True) + forma_pago = TextField(default='') + condiciones_pago = TextField(default='') + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + moneda = TextField(default='MXN') + tipo_cambio = DecimalField(default=1.0, max_digits=15, decimal_places=6, + auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + tipo_comprobante = TextField(default='I') + metodo_pago = TextField(default='PUE') + lugar_expedicion = TextField(default='') + confirmacion = TextField(default='') + uso_cfdi = TextField(default='') + total_retenciones = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + total_trasladados = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + xml = TextField(default='') + uuid = UUIDField(null=True) + estatus = TextField(default='Guardada') + estatus_sat = TextField(default='Vigente') + regimen_fiscal = TextField(default='') + notas = TextField(default='') + saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + pagada = BooleanField(default=False) + cancelada = BooleanField(default=False) + fecha_cancelacion = DateTimeField(null=True) + acuse = TextField(default='') + tipo_relacion = TextField(default='') + error = TextField(default='') + version = TextField(default=CURRENT_CFDI_NOMINA) + registro_patronal = TextField(default='') + rfc_patron_origen = TextField(default='') + tipo_nomina = ForeignKeyField(SATTipoNomina) + fecha_pago = DateField() + fecha_inicial_pago = DateField() + fecha_final_pago = DateField() + dias_pagados = DecimalField(default=0.0, max_digits=12, decimal_places=2, + auto_round=True) + origen_recurso = ForeignKeyField(SATOrigenRecurso, null=True) + monto_recurso_propio = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + class Meta: + order_by = ('fecha',) + + +class CfdiNominaDetalle(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + clave_sat = TextField(default=DEFAULT_SAT_NOMINA['CLAVE']) + cantidad = DecimalField(default=1.0, max_digits=18, decimal_places=6, + auto_round=True) + clave_unidad = TextField(default=DEFAULT_SAT_NOMINA['UNIDAD']) + descripcion = TextField(default=DEFAULT_SAT_NOMINA['DESCRIPCION']) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('cfdi',) + + +class CfdiNominaTotales(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_percepciones = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total_gravado = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_exento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_deducciones = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_otros_pagos = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_sueldos = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_separacion = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_jubilacion = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_retenciones = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_otras_deducciones = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + +class CfdiNominaJubilacion(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + total_una_exhibicion = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + total_parcialidad = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + monto_diario = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_no_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaSeparacion(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + total_pagado = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + years_servicio = IntegerField(default=0) + ultimo_sueldo = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + ingreso_no_acumulable = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaPercepciones(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + tipo_percepcion = ForeignKeyField(SATTipoPercepcion) + importe_gravado = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + importe_exento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + valor_mercado = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + precio_al_ortorgarse = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaDeducciones(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + tipo_deduccion = ForeignKeyField(SATTipoDeduccion) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + +class CfdiNominaOtroPago(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + tipo_otro_pago = ForeignKeyField(SATTipoOtroPago) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + subsidio_causado = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + saldo_a_favor = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + year = IntegerField(default=0) + remanente_saldo = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + +class CfdiNominaIncapacidad(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + dias = IntegerField(default=0) + tipo = ForeignKeyField(SATTipoIncapacidad) + importe = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaHorasExtra(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + dias = IntegerField(default=0) + tipos_horas = ForeignKeyField(SATTipoHoras) + horas_extra = IntegerField(default=0) + importe_pagado = DecimalField(default=0.0, max_digits=20, + decimal_places=6, auto_round=True) + + +class CfdiNominaSubcontratos(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + rfc = TextField(default='') + porcentaje = DecimalField(default=0.0, max_digits=12, decimal_places=2, + auto_round=True) + + class Meta: + order_by = ('cfdi',) + + +class CfdiNominaOtros(BaseModel): + cfdi = ForeignKeyField(CfdiNomina) + node = TextField(default='') + key = TextField(default='') + value = TextField(default='') + + +class CfdiNominaRelacionados(BaseModel): + cfdi = ForeignKeyField(CfdiNomina, related_name='original') + cfdi_origen = ForeignKeyField(CfdiNomina, related_name='relacion') + + class Meta: + order_by = ('cfdi',) + + def authenticate(args): respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''} values = util.get_con(args['rfc']) @@ -5114,14 +5588,22 @@ def _crear_tablas(rfc): PreFacturasRelacionadas, Tickets, TicketsDetalle, TicketsImpuestos, SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes, SATTipoRelacion, SATUnidades, SATUsoCfdi, SATBancos, - SATNivelesEducativos, - Socios, Contactos, ContactoCorreos, ContactoDirecciones, - ContactoTelefonos, + SATNivelesEducativos, SATEstados, SATRiesgoPuesto, SATPeriodicidadPago, + SATOrigenRecurso, SATTipoContrato, SATTipoDeduccion, SATTipoHoras, + SATTipoIncapacidad, SATTipoJornada, SATTipoNomina, SATTipoOtroPago, + SATTipoPercepcion, SATTipoRegimen, + Socios, Contactos, ContactoCorreos, ContactoDirecciones, Empleados, + ContactoTelefonos, Departamentos, Puestos, Tags, Usuarios, CuentasBanco, TipoCambio, MovimientosBanco, TipoCorreo, TipoDireccion, TipoPariente, TipoResponsable, TipoTelefono, TipoTitulo, TipoMovimientoAlumno, TipoMovimientoAlmacen, CfdiPagos, NivelesEducativos, Alumnos, AlumnosParientes, Grupos, ParienteDirecciones, ParienteTelefonos, ParienteCorreos, + CfdiNomina, CfdiNominaDeducciones, CfdiNominaDetalle, + CfdiNominaHorasExtra, CfdiNominaIncapacidad, CfdiNominaJubilacion, + CfdiNominaOtroPago, CfdiNominaOtros, CfdiNominaPercepciones, + CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos, + CfdiNominaTotales, Emisor.regimenes.get_through_model(), Socios.tags.get_through_model(), Productos.impuestos.get_through_model(), @@ -5166,7 +5648,16 @@ def _migrate_tables(): conectar(args) - tablas = [Sucursales] + tablas = [Sucursales, SATEstados, SATRiesgoPuesto, SATPeriodicidadPago, + SATOrigenRecurso, SATTipoContrato, SATTipoDeduccion, SATTipoHoras, + SATTipoIncapacidad, SATTipoJornada, SATTipoNomina, SATTipoOtroPago, + SATTipoPercepcion, SATTipoRegimen, Departamentos, Puestos, Empleados, + CfdiNomina, CfdiNominaDeducciones, CfdiNominaDetalle, + CfdiNominaHorasExtra, CfdiNominaIncapacidad, CfdiNominaJubilacion, + CfdiNominaOtroPago, CfdiNominaOtros, CfdiNominaPercepciones, + CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos, + CfdiNominaTotales, + ] log.info('Creando nuevas tablas...') database_proxy.create_tables(tablas, True) log.info('Tablas creadas correctamente...') @@ -5175,12 +5666,23 @@ def _migrate_tables(): migrations = [] migrator = PostgresqlMigrator(database_proxy) - sucursal = ForeignKeyField(Sucursales, null=True, to_field=Sucursales.id) columns = [c.name for c in database_proxy.get_columns('usuarios')] if not 'sucursal_id' in columns: + sucursal = ForeignKeyField(Sucursales, null=True, to_field=Sucursales.id) migrations.append( migrator.add_column('usuarios', 'sucursal_id', sucursal)) + columns = [c.name for c in database_proxy.get_columns('emisor')] + if not 'registro_patronal' in columns: + registro_patronal = TextField(default='') + migrations.append( + migrator.add_column( + 'emisor', 'registro_patronal', registro_patronal)) + if not 'curp' in columns: + curp = TextField(default='') + migrations.append( + migrator.add_column('emisor', 'curp', curp)) + if migrations: with database_proxy.atomic() as txn: migrate(*migrations) @@ -5732,7 +6234,7 @@ def _importar_factura_libre_gambas(conexion): _importar_socios(data['Socios']) _importar_facturas(data['Facturas']) _importar_categorias(data['Categorias']) - # ~ _importar_productos_gambas(data['Productos']) + _importar_productos_gambas(data['Productos']) _import_tickets(data['Tickets']) log.info('Importación terminada...') @@ -5907,6 +6409,41 @@ def _importar_productos(archivo): return +def _import_from_folder(path): + files = util.get_files(path, 'json') + if not files: + msg = 'No se encontraron archivos para importar' + log.error(msg) + return + + 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 valores...') + for p in files: + msg = '\tImportando tabla: {}' + data = util.import_json(p) + log.info(msg.format(data['tabla'])) + table = globals()[data['tabla']] + for r in data['datos']: + try: + with database_proxy.atomic() as txn: + table.create(**r) + except IntegrityError: + pass + + log.info('Valores importados...') + return + + def _test(): rfc = input('Introduce el RFC: ').strip().upper() if not rfc: @@ -5957,10 +6494,12 @@ help_lr = 'Listar RFCs' @click.option('-a', '--alta', is_flag=True, default=False) @click.option('-r', '--rfc') @click.option('-d', '--detalle', is_flag=True, default=False) +@click.option('-id', '--importar-directorio') def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, agregar_rfc, borrar_rfc, listar_rfc, importar_valores, archivo, conexion, factura_libre, factura_libre_gambas, test, generar_archivo_productos, - importar_productos, backup_dbs, no_bd, alta, rfc, detalle): + importar_productos, backup_dbs, no_bd, alta, rfc, detalle, + importar_directorio): opt = locals() @@ -6066,6 +6605,9 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, _importar_productos(opt['archivo']) sys.exit(0) + if opt['importar_directorio']: + _import_from_folder(opt['importar_directorio']) + if opt['backup_dbs']: util.backup_dbs() diff --git a/source/app/settings.py b/source/app/settings.py index f27a2dc..4f2dcb4 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -115,6 +115,7 @@ PRE = { } CURRENT_CFDI = '3.3' +CURRENT_CFDI_NOMINA = '1.2' DECIMALES = 2 DECIMALES_TAX = 4 IMPUESTOS = { @@ -134,4 +135,9 @@ DEFAULT_SERIE_TICKET = 'T' DIR_FACTURAS = 'facturas' USAR_TOKEN = False CANCEL_SIGNATURE = False -PUBLIC = 'Público en general' \ No newline at end of file +PUBLIC = 'Público en general' +DEFAULT_SAT_NOMINA = { + 'CLAVE': '84111505', + 'UNIDAD': 'ACT', + 'DESCRIPCION': 'Pago de nómina', +} \ No newline at end of file diff --git a/source/db/valores_iniciales.json b/source/db/valores_iniciales.json index 28e6252..b7c4274 100644 --- a/source/db/valores_iniciales.json +++ b/source/db/valores_iniciales.json @@ -240,5 +240,1857 @@ {"key": "D10", "name": "Pagos por servicios educativos (colegiaturas)", "activo": true}, {"key": "P01", "name": "Por definir", "moral": true, "activo": true} ] +}, +{ + "datos": [ + { + "key": "AGU", + "name": "Aguascalientes", + "pais": "MEX" + }, + { + "key": "BCN", + "name": "Baja California", + "pais": "MEX" + }, + { + "key": "BCS", + "name": "Baja California Sur", + "pais": "MEX" + }, + { + "key": "CAM", + "name": "Campeche", + "pais": "MEX" + }, + { + "key": "CHP", + "name": "Chiapas", + "pais": "MEX" + }, + { + "key": "CHH", + "name": "Chihuahua", + "pais": "MEX" + }, + { + "key": "COA", + "name": "Coahuila", + "pais": "MEX" + }, + { + "key": "COL", + "name": "Colima", + "pais": "MEX" + }, + { + "key": "DIF", + "name": "Ciudad de M\u00e9xico", + "pais": "MEX" + }, + { + "key": "DUR", + "name": "Durango", + "pais": "MEX" + }, + { + "key": "GUA", + "name": "Guanajuato", + "pais": "MEX" + }, + { + "key": "GRO", + "name": "Guerrero", + "pais": "MEX" + }, + { + "key": "HID", + "name": "Hidalgo", + "pais": "MEX" + }, + { + "key": "JAL", + "name": "Jalisco", + "pais": "MEX" + }, + { + "key": "MEX", + "name": "Estado de M\u00e9xico", + "pais": "MEX" + }, + { + "key": "MIC", + "name": "Michoac\u00e1n", + "pais": "MEX" + }, + { + "key": "MOR", + "name": "Morelos", + "pais": "MEX" + }, + { + "key": "NAY", + "name": "Nayarit", + "pais": "MEX" + }, + { + "key": "NLE", + "name": "Nuevo Le\u00f3n", + "pais": "MEX" + }, + { + "key": "OAX", + "name": "Oaxaca", + "pais": "MEX" + }, + { + "key": "PUE", + "name": "Puebla", + "pais": "MEX" + }, + { + "key": "QUE", + "name": "Quer\u00e9taro", + "pais": "MEX" + }, + { + "key": "ROO", + "name": "Quintana Roo", + "pais": "MEX" + }, + { + "key": "SLP", + "name": "San Luis Potos\u00ed", + "pais": "MEX" + }, + { + "key": "SIN", + "name": "Sinaloa", + "pais": "MEX" + }, + { + "key": "SON", + "name": "Sonora", + "pais": "MEX" + }, + { + "key": "TAB", + "name": "Tabasco", + "pais": "MEX" + }, + { + "key": "TAM", + "name": "Tamaulipas", + "pais": "MEX" + }, + { + "key": "TLA", + "name": "Tlaxcala", + "pais": "MEX" + }, + { + "key": "VER", + "name": "Veracruz", + "pais": "MEX" + }, + { + "key": "YUC", + "name": "Yucat\u00e1n", + "pais": "MEX" + }, + { + "key": "ZAC", + "name": "Zacatecas", + "pais": "MEX" + }, + { + "key": "AL", + "name": "Alabama", + "pais": "USA" + }, + { + "key": "AK", + "name": "Alaska", + "pais": "USA" + }, + { + "key": "AZ", + "name": "Arizona", + "pais": "USA" + }, + { + "key": "AR", + "name": "Arkansas", + "pais": "USA" + }, + { + "key": "CA", + "name": "California", + "pais": "USA" + }, + { + "key": "NC", + "name": "Carolina del Norte", + "pais": "USA" + }, + { + "key": "SC", + "name": "Carolina del Sur", + "pais": "USA" + }, + { + "key": "CO", + "name": "Colorado", + "pais": "USA" + }, + { + "key": "CT", + "name": "Connecticut", + "pais": "USA" + }, + { + "key": "ND", + "name": "Dakota del Norte", + "pais": "USA" + }, + { + "key": "SD", + "name": "Dakota del Sur", + "pais": "USA" + }, + { + "key": "DE", + "name": "Delaware", + "pais": "USA" + }, + { + "key": "FL", + "name": "Florida", + "pais": "USA" + }, + { + "key": "GA", + "name": "Georgia", + "pais": "USA" + }, + { + "key": "HI", + "name": "Haw\u00e1i", + "pais": "USA" + }, + { + "key": "ID", + "name": "Idaho", + "pais": "USA" + }, + { + "key": "IL", + "name": "Illinois", + "pais": "USA" + }, + { + "key": "IN", + "name": "Indiana", + "pais": "USA" + }, + { + "key": "IA", + "name": "Iowa", + "pais": "USA" + }, + { + "key": "KS", + "name": "Kansas", + "pais": "USA" + }, + { + "key": "KY", + "name": "Kentucky", + "pais": "USA" + }, + { + "key": "LA", + "name": "Luisiana", + "pais": "USA" + }, + { + "key": "ME", + "name": "Maine", + "pais": "USA" + }, + { + "key": "MD", + "name": "Maryland", + "pais": "USA" + }, + { + "key": "MA", + "name": "Massachusetts", + "pais": "USA" + }, + { + "key": "MI", + "name": "M\u00edchigan", + "pais": "USA" + }, + { + "key": "MN", + "name": "Minnesota", + "pais": "USA" + }, + { + "key": "MS", + "name": "Misisipi", + "pais": "USA" + }, + { + "key": "MO", + "name": "Misuri", + "pais": "USA" + }, + { + "key": "MT", + "name": "Montana", + "pais": "USA" + }, + { + "key": "NE", + "name": "Nebraska", + "pais": "USA" + }, + { + "key": "NV", + "name": "Nevada", + "pais": "USA" + }, + { + "key": "NJ", + "name": "Nueva Jersey", + "pais": "USA" + }, + { + "key": "NY", + "name": "Nueva York", + "pais": "USA" + }, + { + "key": "NH", + "name": "Nuevo Hampshire", + "pais": "USA" + }, + { + "key": "NM", + "name": "Nuevo M\u00e9xico", + "pais": "USA" + }, + { + "key": "OH", + "name": "Ohio", + "pais": "USA" + }, + { + "key": "OK", + "name": "Oklahoma", + "pais": "USA" + }, + { + "key": "OR", + "name": "Oreg\u00f3n", + "pais": "USA" + }, + { + "key": "PA", + "name": "Pensilvania", + "pais": "USA" + }, + { + "key": "RI", + "name": "Rhode Island", + "pais": "USA" + }, + { + "key": "TN", + "name": "Tennessee", + "pais": "USA" + }, + { + "key": "TX", + "name": "Texas", + "pais": "USA" + }, + { + "key": "UT", + "name": "Utah", + "pais": "USA" + }, + { + "key": "VT", + "name": "Vermont", + "pais": "USA" + }, + { + "key": "VA", + "name": "Virginia", + "pais": "USA" + }, + { + "key": "WV", + "name": "Virginia Occidental", + "pais": "USA" + }, + { + "key": "WA", + "name": "Washington", + "pais": "USA" + }, + { + "key": "WI", + "name": "Wisconsin", + "pais": "USA" + }, + { + "key": "WY", + "name": "Wyoming", + "pais": "USA" + }, + { + "key": "ON", + "name": "Ontario\u00a0", + "pais": "CAN" + }, + { + "key": "QC", + "name": "Quebec\u00a0", + "pais": "CAN" + }, + { + "key": "NS", + "name": "Nueva Escocia", + "pais": "CAN" + }, + { + "key": "NB", + "name": "Nuevo Brunswick\u00a0", + "pais": "CAN" + }, + { + "key": "MB", + "name": "Manitoba", + "pais": "CAN" + }, + { + "key": "BC", + "name": "Columbia Brit\u00e1nica", + "pais": "CAN" + }, + { + "key": "PE", + "name": "Isla del Pr\u00edncipe Eduardo", + "pais": "CAN" + }, + { + "key": "SK", + "name": "Saskatchewan", + "pais": "CAN" + }, + { + "key": "AB", + "name": "Alberta", + "pais": "CAN" + }, + { + "key": "NL", + "name": "Terranova y Labrador", + "pais": "CAN" + }, + { + "key": "NT", + "name": "Territorios del Noroeste", + "pais": "CAN" + }, + { + "key": "YT", + "name": "Yuk\u00f3n", + "pais": "CAN" + }, + { + "key": "UN", + "name": "Nunavut", + "pais": "CAN" + } + ], + "tabla": "SATEstados" +}, +{ + "datos": [ + { + "key": "AGU", + "name": "Aguascalientes", + "pais": "MEX" + }, + { + "key": "BCN", + "name": "Baja California", + "pais": "MEX" + }, + { + "key": "BCS", + "name": "Baja California Sur", + "pais": "MEX" + }, + { + "key": "CAM", + "name": "Campeche", + "pais": "MEX" + }, + { + "key": "CHP", + "name": "Chiapas", + "pais": "MEX" + }, + { + "key": "CHH", + "name": "Chihuahua", + "pais": "MEX" + }, + { + "key": "COA", + "name": "Coahuila", + "pais": "MEX" + }, + { + "key": "COL", + "name": "Colima", + "pais": "MEX" + }, + { + "key": "DIF", + "name": "Ciudad de M\u00e9xico", + "pais": "MEX" + }, + { + "key": "DUR", + "name": "Durango", + "pais": "MEX" + }, + { + "key": "GUA", + "name": "Guanajuato", + "pais": "MEX" + }, + { + "key": "GRO", + "name": "Guerrero", + "pais": "MEX" + }, + { + "key": "HID", + "name": "Hidalgo", + "pais": "MEX" + }, + { + "key": "JAL", + "name": "Jalisco", + "pais": "MEX" + }, + { + "key": "MEX", + "name": "Estado de M\u00e9xico", + "pais": "MEX" + }, + { + "key": "MIC", + "name": "Michoac\u00e1n", + "pais": "MEX" + }, + { + "key": "MOR", + "name": "Morelos", + "pais": "MEX" + }, + { + "key": "NAY", + "name": "Nayarit", + "pais": "MEX" + }, + { + "key": "NLE", + "name": "Nuevo Le\u00f3n", + "pais": "MEX" + }, + { + "key": "OAX", + "name": "Oaxaca", + "pais": "MEX" + }, + { + "key": "PUE", + "name": "Puebla", + "pais": "MEX" + }, + { + "key": "QUE", + "name": "Quer\u00e9taro", + "pais": "MEX" + }, + { + "key": "ROO", + "name": "Quintana Roo", + "pais": "MEX" + }, + { + "key": "SLP", + "name": "San Luis Potos\u00ed", + "pais": "MEX" + }, + { + "key": "SIN", + "name": "Sinaloa", + "pais": "MEX" + }, + { + "key": "SON", + "name": "Sonora", + "pais": "MEX" + }, + { + "key": "TAB", + "name": "Tabasco", + "pais": "MEX" + }, + { + "key": "TAM", + "name": "Tamaulipas", + "pais": "MEX" + }, + { + "key": "TLA", + "name": "Tlaxcala", + "pais": "MEX" + }, + { + "key": "VER", + "name": "Veracruz", + "pais": "MEX" + }, + { + "key": "YUC", + "name": "Yucat\u00e1n", + "pais": "MEX" + }, + { + "key": "ZAC", + "name": "Zacatecas", + "pais": "MEX" + }, + { + "key": "AL", + "name": "Alabama", + "pais": "USA" + }, + { + "key": "AK", + "name": "Alaska", + "pais": "USA" + }, + { + "key": "AZ", + "name": "Arizona", + "pais": "USA" + }, + { + "key": "AR", + "name": "Arkansas", + "pais": "USA" + }, + { + "key": "CA", + "name": "California", + "pais": "USA" + }, + { + "key": "NC", + "name": "Carolina del Norte", + "pais": "USA" + }, + { + "key": "SC", + "name": "Carolina del Sur", + "pais": "USA" + }, + { + "key": "CO", + "name": "Colorado", + "pais": "USA" + }, + { + "key": "CT", + "name": "Connecticut", + "pais": "USA" + }, + { + "key": "ND", + "name": "Dakota del Norte", + "pais": "USA" + }, + { + "key": "SD", + "name": "Dakota del Sur", + "pais": "USA" + }, + { + "key": "DE", + "name": "Delaware", + "pais": "USA" + }, + { + "key": "FL", + "name": "Florida", + "pais": "USA" + }, + { + "key": "GA", + "name": "Georgia", + "pais": "USA" + }, + { + "key": "HI", + "name": "Haw\u00e1i", + "pais": "USA" + }, + { + "key": "ID", + "name": "Idaho", + "pais": "USA" + }, + { + "key": "IL", + "name": "Illinois", + "pais": "USA" + }, + { + "key": "IN", + "name": "Indiana", + "pais": "USA" + }, + { + "key": "IA", + "name": "Iowa", + "pais": "USA" + }, + { + "key": "KS", + "name": "Kansas", + "pais": "USA" + }, + { + "key": "KY", + "name": "Kentucky", + "pais": "USA" + }, + { + "key": "LA", + "name": "Luisiana", + "pais": "USA" + }, + { + "key": "ME", + "name": "Maine", + "pais": "USA" + }, + { + "key": "MD", + "name": "Maryland", + "pais": "USA" + }, + { + "key": "MA", + "name": "Massachusetts", + "pais": "USA" + }, + { + "key": "MI", + "name": "M\u00edchigan", + "pais": "USA" + }, + { + "key": "MN", + "name": "Minnesota", + "pais": "USA" + }, + { + "key": "MS", + "name": "Misisipi", + "pais": "USA" + }, + { + "key": "MO", + "name": "Misuri", + "pais": "USA" + }, + { + "key": "MT", + "name": "Montana", + "pais": "USA" + }, + { + "key": "NE", + "name": "Nebraska", + "pais": "USA" + }, + { + "key": "NV", + "name": "Nevada", + "pais": "USA" + }, + { + "key": "NJ", + "name": "Nueva Jersey", + "pais": "USA" + }, + { + "key": "NY", + "name": "Nueva York", + "pais": "USA" + }, + { + "key": "NH", + "name": "Nuevo Hampshire", + "pais": "USA" + }, + { + "key": "NM", + "name": "Nuevo M\u00e9xico", + "pais": "USA" + }, + { + "key": "OH", + "name": "Ohio", + "pais": "USA" + }, + { + "key": "OK", + "name": "Oklahoma", + "pais": "USA" + }, + { + "key": "OR", + "name": "Oreg\u00f3n", + "pais": "USA" + }, + { + "key": "PA", + "name": "Pensilvania", + "pais": "USA" + }, + { + "key": "RI", + "name": "Rhode Island", + "pais": "USA" + }, + { + "key": "TN", + "name": "Tennessee", + "pais": "USA" + }, + { + "key": "TX", + "name": "Texas", + "pais": "USA" + }, + { + "key": "UT", + "name": "Utah", + "pais": "USA" + }, + { + "key": "VT", + "name": "Vermont", + "pais": "USA" + }, + { + "key": "VA", + "name": "Virginia", + "pais": "USA" + }, + { + "key": "WV", + "name": "Virginia Occidental", + "pais": "USA" + }, + { + "key": "WA", + "name": "Washington", + "pais": "USA" + }, + { + "key": "WI", + "name": "Wisconsin", + "pais": "USA" + }, + { + "key": "WY", + "name": "Wyoming", + "pais": "USA" + }, + { + "key": "ON", + "name": "Ontario\u00a0", + "pais": "CAN" + }, + { + "key": "QC", + "name": "Quebec\u00a0", + "pais": "CAN" + }, + { + "key": "NS", + "name": "Nueva Escocia", + "pais": "CAN" + }, + { + "key": "NB", + "name": "Nuevo Brunswick\u00a0", + "pais": "CAN" + }, + { + "key": "MB", + "name": "Manitoba", + "pais": "CAN" + }, + { + "key": "BC", + "name": "Columbia Brit\u00e1nica", + "pais": "CAN" + }, + { + "key": "PE", + "name": "Isla del Pr\u00edncipe Eduardo", + "pais": "CAN" + }, + { + "key": "SK", + "name": "Saskatchewan", + "pais": "CAN" + }, + { + "key": "AB", + "name": "Alberta", + "pais": "CAN" + }, + { + "key": "NL", + "name": "Terranova y Labrador", + "pais": "CAN" + }, + { + "key": "NT", + "name": "Territorios del Noroeste", + "pais": "CAN" + }, + { + "key": "YT", + "name": "Yuk\u00f3n", + "pais": "CAN" + }, + { + "key": "UN", + "name": "Nunavut", + "pais": "CAN" + } + ], + "tabla": "SATEstados" +}, +{ + "datos": [ + { + "key": "IP", + "name": "Ingresos propios." + }, + { + "key": "IF", + "name": "Ingreso federales." + }, + { + "key": "IM", + "name": "Ingresos mixtos." + } + ], + "tabla": "SATOrigenRecurso" +}, +{ + "datos": [ + { + "key": "01", + "name": "Diario" + }, + { + "key": "02", + "name": "Semanal" + }, + { + "key": "03", + "name": "Catorcenal" + }, + { + "key": "04", + "name": "Quincenal" + }, + { + "key": "05", + "name": "Mensual" + }, + { + "key": "06", + "name": "Bimestral" + }, + { + "key": "07", + "name": "Unidad obra" + }, + { + "key": "08", + "name": "Comisi\u00f3n" + }, + { + "key": "09", + "name": "Precio alzado" + }, + { + "key": "10", + "name": "Decenal" + }, + { + "key": "99", + "name": "Otra Periodicidad" + } + ], + "tabla": "SATPeriodicidadPago" +}, +{ + "datos": [ + { + "key": "1", + "name": "Clase I" + }, + { + "key": "2", + "name": "Clase II" + }, + { + "key": "3", + "name": "Clase III" + }, + { + "key": "4", + "name": "Clase IV" + }, + { + "key": "5", + "name": "Clase V" + }, + { + "key": "99", + "name": "No aplica" + } + ], + "tabla": "SATRiesgoPuesto" +}, +{ + "datos": [ + { + "key": "01", + "name": "Contrato de trabajo por tiempo indeterminado" + }, + { + "key": "02", + "name": "Contrato de trabajo para obra determinada" + }, + { + "key": "03", + "name": "Contrato de trabajo por tiempo determinado" + }, + { + "key": "04", + "name": "Contrato de trabajo por temporada" + }, + { + "key": "05", + "name": "Contrato de trabajo sujeto a prueba" + }, + { + "key": "06", + "name": "Contrato de trabajo con capacitaci\u00f3n inicial" + }, + { + "key": "07", + "name": "Modalidad de contrataci\u00f3n por pago de hora laborada" + }, + { + "key": "08", + "name": "Modalidad de trabajo por comisi\u00f3n laboral" + }, + { + "key": "09", + "name": "Modalidades de contrataci\u00f3n donde no existe relaci\u00f3n de trabajo" + }, + { + "key": "10", + "name": "Jubilaci\u00f3n, pensi\u00f3n, retiro." + }, + { + "key": "99", + "name": "Otro contrato" + } + ], + "tabla": "SATTipoContrato" +}, +{ + "datos": [ + { + "key": "001", + "name": "Seguridad social" + }, + { + "key": "002", + "name": "ISR" + }, + { + "key": "003", + "name": "Aportaciones a retiro, cesant\u00eda en edad avanzada y vejez." + }, + { + "key": "004", + "name": "Otros" + }, + { + "key": "005", + "name": "Aportaciones a Fondo de vivienda" + }, + { + "key": "006", + "name": "Descuento por incapacidad" + }, + { + "key": "007", + "name": "Pensi\u00f3n alimenticia" + }, + { + "key": "008", + "name": "Renta" + }, + { + "key": "009", + "name": "Pr\u00e9stamos provenientes del Fondo Nacional de la Vivienda para los Trabajadores" + }, + { + "key": "010", + "name": "Pago por cr\u00e9dito de vivienda" + }, + { + "key": "011", + "name": "Pago de abonos INFONACOT" + }, + { + "key": "012", + "name": "Anticipo de salarios" + }, + { + "key": "013", + "name": "Pagos hechos con exceso al trabajador" + }, + { + "key": "014", + "name": "Errores" + }, + { + "key": "015", + "name": "P\u00e9rdidas" + }, + { + "key": "016", + "name": "Aver\u00edas" + }, + { + "key": "017", + "name": "Adquisici\u00f3n de art\u00edculos producidos por la empresa o establecimiento" + }, + { + "key": "018", + "name": "Cuotas para la constituci\u00f3n y fomento de sociedades cooperativas y de cajas de ahorro" + }, + { + "key": "019", + "name": "Cuotas sindicales" + }, + { + "key": "020", + "name": "Ausencia (Ausentismo)" + }, + { + "key": "021", + "name": "Cuotas obrero patronales" + }, + { + "key": "022", + "name": "Impuestos Locales" + }, + { + "key": "023", + "name": "Aportaciones voluntarias" + }, + { + "key": "024", + "name": "Ajuste en Gratificaci\u00f3n Anual (Aguinaldo) Exento" + }, + { + "key": "025", + "name": "Ajuste en Gratificaci\u00f3n Anual (Aguinaldo) Gravado" + }, + { + "key": "026", + "name": "Ajuste en Participaci\u00f3n de los Trabajadores en las Utilidades PTU Exento" + }, + { + "key": "027", + "name": "Ajuste en Participaci\u00f3n de los Trabajadores en las Utilidades PTU Gravado" + }, + { + "key": "028", + "name": "Ajuste en Reembolso de Gastos M\u00e9dicos Dentales y Hospitalarios Exento" + }, + { + "key": "029", + "name": "Ajuste en Fondo de ahorro Exento" + }, + { + "key": "030", + "name": "Ajuste en Caja de ahorro Exento" + }, + { + "key": "031", + "name": "Ajuste en Contribuciones a Cargo del Trabajador Pagadas por el Patr\u00f3n Exento" + }, + { + "key": "032", + "name": "Ajuste en Premios por puntualidad Gravado" + }, + { + "key": "033", + "name": "Ajuste en Prima de Seguro de vida Exento" + }, + { + "key": "034", + "name": "Ajuste en Seguro de Gastos M\u00e9dicos Mayores Exento" + }, + { + "key": "035", + "name": "Ajuste en Cuotas Sindicales Pagadas por el Patr\u00f3n Exento" + }, + { + "key": "036", + "name": "Ajuste en Subsidios por incapacidad Exento" + }, + { + "key": "037", + "name": "Ajuste en Becas para trabajadores y/o hijos Exento" + }, + { + "key": "038", + "name": "Ajuste en Horas extra Exento" + }, + { + "key": "039", + "name": "Ajuste en Horas extra Gravado" + }, + { + "key": "040", + "name": "Ajuste en Prima dominical Exento" + }, + { + "key": "041", + "name": "Ajuste en Prima dominical Gravado" + }, + { + "key": "042", + "name": "Ajuste en Prima vacacional Exento" + }, + { + "key": "043", + "name": "Ajuste en Prima vacacional Gravado" + }, + { + "key": "044", + "name": "Ajuste en Prima por antig\u00fcedad Exento" + }, + { + "key": "045", + "name": "Ajuste en Prima por antig\u00fcedad Gravado" + }, + { + "key": "046", + "name": "Ajuste en Pagos por separaci\u00f3n Exento" + }, + { + "key": "047", + "name": "Ajuste en Pagos por separaci\u00f3n Gravado" + }, + { + "key": "048", + "name": "Ajuste en Seguro de retiro Exento" + }, + { + "key": "049", + "name": "Ajuste en Indemnizaciones Exento" + }, + { + "key": "050", + "name": "Ajuste en Indemnizaciones Gravado" + }, + { + "key": "051", + "name": "Ajuste en Reembolso por funeral Exento" + }, + { + "key": "052", + "name": "Ajuste en Cuotas de seguridad social pagadas por el patr\u00f3n Exento" + }, + { + "key": "053", + "name": "Ajuste en Comisiones Gravado" + }, + { + "key": "054", + "name": "Ajuste en Vales de despensa Exento" + }, + { + "key": "055", + "name": "Ajuste en Vales de restaurante Exento" + }, + { + "key": "056", + "name": "Ajuste en Vales de gasolina Exento" + }, + { + "key": "057", + "name": "Ajuste en Vales de ropa Exento" + }, + { + "key": "058", + "name": "Ajuste en Ayuda para renta Exento" + }, + { + "key": "059", + "name": "Ajuste en Ayuda para art\u00edculos escolares Exento" + }, + { + "key": "060", + "name": "Ajuste en Ayuda para anteojos Exento" + }, + { + "key": "061", + "name": "Ajuste en Ayuda para transporte Exento" + }, + { + "key": "062", + "name": "Ajuste en Ayuda para gastos de funeral Exento" + }, + { + "key": "063", + "name": "Ajuste en Otros ingresos por salarios Exento" + }, + { + "key": "064", + "name": "Ajuste en Otros ingresos por salarios Gravado" + }, + { + "key": "065", + "name": "Ajuste en Jubilaciones, pensiones o haberes de retiro Exento" + }, + { + "key": "066", + "name": "Ajuste en Jubilaciones, pensiones o haberes de retiro Gravado" + }, + { + "key": "067", + "name": "Ajuste en Pagos por separaci\u00f3n Acumulable" + }, + { + "key": "068", + "name": "Ajuste en Pagos por separaci\u00f3n No acumulable" + }, + { + "key": "069", + "name": "Ajuste en Jubilaciones, pensiones o haberes de retiro Acumulable" + }, + { + "key": "070", + "name": "Ajuste en Jubilaciones, pensiones o haberes de retiro No acumulable" + }, + { + "key": "071", + "name": "Ajuste en Subsidio para el empleo (efectivamente entregado al trabajador)" + }, + { + "key": "072", + "name": "Ajuste en Ingresos en acciones o t\u00edtulos valor que representan bienes Exento" + }, + { + "key": "073", + "name": "Ajuste en Ingresos en acciones o t\u00edtulos valor que representan bienes Gravado" + }, + { + "key": "074", + "name": "Ajuste en Alimentaci\u00f3n Exento" + }, + { + "key": "075", + "name": "Ajuste en Alimentaci\u00f3n Gravado" + }, + { + "key": "076", + "name": "Ajuste en Habitaci\u00f3n Exento" + }, + { + "key": "077", + "name": "Ajuste en Habitaci\u00f3n Gravado" + }, + { + "key": "078", + "name": "Ajuste en Premios por asistencia" + }, + { + "key": "079", + "name": "Ajuste en Pagos distintos a los listados y que no deben considerarse como ingreso por sueldos, salarios o ingresos asimilados." + }, + { + "key": "080", + "name": "Ajuste en Vi\u00e1ticos gravados" + }, + { + "key": "081", + "name": "Ajuste en Vi\u00e1ticos (entregados al trabajador)" + }, + { + "key": "082", + "name": "Ajuste en Fondo de ahorro Gravado" + }, + { + "key": "083", + "name": "Ajuste en Caja de ahorro Gravado" + }, + { + "key": "084", + "name": "Ajuste en Prima de Seguro de vida Gravado" + }, + { + "key": "085", + "name": "Ajuste en Seguro de Gastos M\u00e9dicos Mayores Gravado" + }, + { + "key": "086", + "name": "Ajuste en Subsidios por incapacidad Gravado" + }, + { + "key": "087", + "name": "Ajuste en Becas para trabajadores y/o hijos Gravado" + }, + { + "key": "088", + "name": "Ajuste en Seguro de retiro Gravado" + }, + { + "key": "089", + "name": "Ajuste en Vales de despensa Gravado" + }, + { + "key": "090", + "name": "Ajuste en Vales de restaurante Gravado" + }, + { + "key": "091", + "name": "Ajuste en Vales de gasolina Gravado" + }, + { + "key": "092", + "name": "Ajuste en Vales de ropa Gravado" + }, + { + "key": "093", + "name": "Ajuste en Ayuda para renta Gravado" + }, + { + "key": "094", + "name": "Ajuste en Ayuda para art\u00edculos escolares Gravado" + }, + { + "key": "095", + "name": "Ajuste en Ayuda para anteojos Gravado" + }, + { + "key": "096", + "name": "Ajuste en Ayuda para transporte Gravado" + }, + { + "key": "097", + "name": "Ajuste en Ayuda para gastos de funeral Gravado" + }, + { + "key": "098", + "name": "Ajuste a ingresos asimilados a salarios gravados" + }, + { + "key": "099", + "name": "Ajuste a ingresos por sueldos y salarios gravados" + }, + { + "key": "100", + "name": "Ajuste en Vi\u00e1ticos exentos" + }, + { + "key": "101", + "name": "ISR Retenido de ejercicio anterior" + } + ], + "tabla": "SATTipoDeduccion" +}, +{ + "datos": [ + { + "key": "01", + "name": "Dobles" + }, + { + "key": "02", + "name": "Triples" + }, + { + "key": "03", + "name": "Simples" + } + ], + "tabla": "SATTipoHoras" +}, +{ + "datos": [ + { + "key": "01", + "name": "Riesgo de trabajo." + }, + { + "key": "02", + "name": "Enfermedad en general." + }, + { + "key": "03", + "name": "Maternidad." + } + ], + "tabla": "SATTipoIncapacidad" +}, +{ + "datos": [ + { + "key": "01", + "name": "Diurna" + }, + { + "key": "02", + "name": "Nocturna" + }, + { + "key": "03", + "name": "Mixta" + }, + { + "key": "04", + "name": "Por hora" + }, + { + "key": "05", + "name": "Reducida" + }, + { + "key": "06", + "name": "Continuada" + }, + { + "key": "07", + "name": "Partida" + }, + { + "key": "08", + "name": "Por turnos" + }, + { + "key": "99", + "name": "Otra Jornada" + } + ], + "tabla": "SATTipoJornada" +}, +{ + "datos": [ + { + "key": "O", + "name": "N\u00f3mina ordinaria" + }, + { + "key": "E", + "name": "N\u00f3mina extraordinaria" + } + ], + "tabla": "SATTipoNomina" +}, +{ + "datos": [ + { + "key": "001", + "name": "Reintegro de ISR pagado en exceso (siempre que no haya sido enterado al SAT)." + }, + { + "key": "002", + "name": "Subsidio para el empleo (efectivamente entregado al trabajador)." + }, + { + "key": "003", + "name": "Vi\u00e1ticos (entregados al trabajador)." + }, + { + "key": "004", + "name": "Aplicaci\u00f3n de saldo a favor por compensaci\u00f3n anual." + }, + { + "key": "005", + "name": "Reintegro de ISR retenido en exceso de ejercicio anterior (siempre que no haya sido enterado al SAT)." + }, + { + "key": "999", + "name": "Pagos distintos a los listados y que no deben considerarse como ingreso por sueldos, salarios o ingresos asimilados." + } + ], + "tabla": "SATTipoOtroPago" +}, +{ + "datos": [ + { + "key": "001", + "name": "Sueldos, Salarios Rayas y Jornales" + }, + { + "key": "002", + "name": "Gratificaci\u00f3n Anual (Aguinaldo)" + }, + { + "key": "003", + "name": "Participaci\u00f3n de los Trabajadores en las Utilidades PTU" + }, + { + "key": "004", + "name": "Reembolso de Gastos M\u00e9dicos Dentales y Hospitalarios" + }, + { + "key": "005", + "name": "Fondo de Ahorro" + }, + { + "key": "006", + "name": "Caja de ahorro" + }, + { + "key": "009", + "name": "Contribuciones a Cargo del Trabajador Pagadas por el Patr\u00f3n" + }, + { + "key": "010", + "name": "Premios por puntualidad" + }, + { + "key": "011", + "name": "Prima de Seguro de vida" + }, + { + "key": "012", + "name": "Seguro de Gastos M\u00e9dicos Mayores" + }, + { + "key": "013", + "name": "Cuotas Sindicales Pagadas por el Patr\u00f3n" + }, + { + "key": "014", + "name": "Subsidios por incapacidad" + }, + { + "key": "015", + "name": "Becas para trabajadores y/o hijos" + }, + { + "key": "019", + "name": "Horas extra" + }, + { + "key": "020", + "name": "Prima dominical" + }, + { + "key": "021", + "name": "Prima vacacional" + }, + { + "key": "022", + "name": "Prima por antig\u00fcedad" + }, + { + "key": "023", + "name": "Pagos por separaci\u00f3n" + }, + { + "key": "024", + "name": "Seguro de retiro" + }, + { + "key": "025", + "name": "Indemnizaciones" + }, + { + "key": "026", + "name": "Reembolso por funeral" + }, + { + "key": "027", + "name": "Cuotas de seguridad social pagadas por el patr\u00f3n" + }, + { + "key": "028", + "name": "Comisiones" + }, + { + "key": "029", + "name": "Vales de despensa" + }, + { + "key": "030", + "name": "Vales de restaurante" + }, + { + "key": "031", + "name": "Vales de gasolina" + }, + { + "key": "032", + "name": "Vales de ropa" + }, + { + "key": "033", + "name": "Ayuda para renta" + }, + { + "key": "034", + "name": "Ayuda para art\u00edculos escolares" + }, + { + "key": "035", + "name": "Ayuda para anteojos" + }, + { + "key": "036", + "name": "Ayuda para transporte" + }, + { + "key": "037", + "name": "Ayuda para gastos de funeral" + }, + { + "key": "038", + "name": "Otros ingresos por salarios" + }, + { + "key": "039", + "name": "Jubilaciones, pensiones o haberes de retiro" + }, + { + "key": "044", + "name": "Jubilaciones, pensiones o haberes de retiro en parcialidades" + }, + { + "key": "045", + "name": "Ingresos en acciones o t\u00edtulos valor que representan bienes" + }, + { + "key": "046", + "name": "Ingresos asimilados a salarios" + }, + { + "key": "047", + "name": "Alimentaci\u00f3n" + }, + { + "key": "048", + "name": "Habitaci\u00f3n" + }, + { + "key": "049", + "name": "Premios por asistencia" + }, + { + "key": "050", + "name": "Vi\u00e1ticos" + } + ], + "tabla": "SATTipoPercepcion" +}, +{ + "datos": [ + { + "key": "02", + "name": "Sueldos" + }, + { + "key": "03", + "name": "Jubilados" + }, + { + "key": "04", + "name": "Pensionados" + }, + { + "key": "05", + "name": "Asimilados Miembros Sociedades Cooperativas Produccion" + }, + { + "key": "06", + "name": "Asimilados Integrantes Sociedades Asociaciones Civiles" + }, + { + "key": "07", + "name": "Asimilados Miembros consejos" + }, + { + "key": "08", + "name": "Asimilados comisionistas" + }, + { + "key": "09", + "name": "Asimilados Honorarios" + }, + { + "key": "10", + "name": "Asimilados acciones" + }, + { + "key": "11", + "name": "Asimilados otros" + }, + { + "key": "12", + "name": "Jubilados o Pensionados" + }, + { + "key": "99", + "name": "Otro Regimen" + } + ], + "tabla": "SATTipoRegimen" } ] From 9c6659193c65cd969207714a6c6a9a23ead31eca Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 15 Jan 2018 23:49:33 -0600 Subject: [PATCH 02/24] =?UTF-8?q?Interfaz=20para=20n=C3=B3mina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/models/main.py | 11 +- source/static/js/controller/admin.js | 8 +- source/static/js/controller/main.js | 21 +++- source/static/js/controller/nomina.js | 52 ++++++++ source/static/js/controller/util.js | 1 + source/static/js/ui/admin.js | 64 ++++++---- source/static/js/ui/main.js | 3 +- source/static/js/ui/nomina.js | 166 ++++++++++++++++++++++++++ source/templates/main.html | 2 + 9 files changed, 295 insertions(+), 33 deletions(-) create mode 100644 source/static/js/controller/nomina.js create mode 100644 source/static/js/ui/nomina.js diff --git a/source/app/models/main.py b/source/app/models/main.py index 1699698..5a9ef2d 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -194,10 +194,11 @@ def config_main(): obj = None punto_de_venta = util.get_bool(Configuracion.get_('chk_usar_punto_de_venta')) + nomina = util.get_bool(Configuracion.get_('chk_usar_nomina')) data = { 'empresa': get_title_app(3), 'punto_de_venta': punto_de_venta - + 'nomina': nomina, } if not obj is None: titulo = '{} - {}' @@ -322,6 +323,7 @@ class Configuracion(BaseModel): 'chk_ticket_direct_print', 'chk_ticket_edit_cant', 'chk_ticket_total_up', + 'chk_usar_nomina', ) data = (Configuracion .select() @@ -697,6 +699,7 @@ class Emisor(BaseModel): obj = obj[0] row['emisor'] = { 'emisor_rfc': obj.rfc, + 'emisor_curp': obj.curp, 'emisor_nombre': obj.nombre, 'emisor_cp': obj.codigo_postal, 'emisor_cp2': obj.cp_expedicion, @@ -720,6 +723,7 @@ class Emisor(BaseModel): 'correo_timbrado': obj.correo_timbrado, 'token_timbrado': obj.token_timbrado, 'token_soporte': obj.token_soporte, + 'emisor_registro_patronal': obj.registro_patronal, 'regimenes': [row.id for row in obj.regimenes] } else: @@ -754,6 +758,7 @@ class Emisor(BaseModel): def _clean(self, values): fields = util.clean(values) fields['rfc'] = fields.pop('emisor_rfc') + fields['curp'] = fields.pop('emisor_curp', '') fields['nombre'] = fields.pop('emisor_nombre') fields['codigo_postal'] = fields.pop('emisor_cp') fields['cp_expedicion'] = fields.pop('emisor_cp2', '') or fields['codigo_postal'] @@ -776,6 +781,7 @@ class Emisor(BaseModel): fields['fecha_dof'] = fields.pop('ong_fecha_dof', None) if len(fields['rfc']) == 12: fields['es_moral'] = True + fields['registro_patronal'] = fields.pop('emisor_registro_patronal', '') fields['regimenes'] = SATRegimenes.get_( util.loads(fields['regimenes'])) return fields @@ -5230,6 +5236,7 @@ class Empleados(BaseModel): estado = ForeignKeyField(SATEstados) codigo_postal = TextField(default='') notas = TextField(default='') + correo = TextField(default='') class Meta: order_by = ('nombre_completo',) @@ -5658,7 +5665,7 @@ def _migrate_tables(): CfdiNominaRelacionados, CfdiNominaSeparacion, CfdiNominaSubcontratos, CfdiNominaTotales, ] - log.info('Creando nuevas tablas...') + log.info('Creando tablas nuevas...') database_proxy.create_tables(tablas, True) log.info('Tablas creadas correctamente...') diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index b7a4ea5..0487030 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -74,6 +74,7 @@ var controllers = { $$('chk_ticket_edit_cant').attachEvent('onItemClick', chk_config_item_click) $$('chk_ticket_total_up').attachEvent('onItemClick', chk_config_item_click) $$('txt_ticket_printer').attachEvent('onKeyPress', txt_ticket_printer_key_press) + $$('chk_usar_nomina').attachEvent('onItemClick', chk_config_item_click) $$('cmd_subir_bdfl').attachEvent('onItemClick', cmd_subir_bdfl_click) $$('up_bdfl').attachEvent('onUploadComplete', up_bdfl_upload_complete) @@ -197,6 +198,9 @@ function get_emisor(){ if(emisor.regimenes){ $$('lst_emisor_regimen').select(emisor.regimenes) } + if(emisor.emisor_rfc.length == 12){ + show('emisor_curp', false) + } }else{ msg_error(values.msg) } @@ -1013,8 +1017,8 @@ function txt_plantilla_donataria_click(e){ function tab_options_change(nv, ov){ var cv = { - Plantillas: 'templates', - Otros: 'configotros', + tab_admin_templates: 'templates', + tab_admin_otros: 'configotros', } get_config_values(cv[nv]) } diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index d2bbf9e..3456c98 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -9,13 +9,21 @@ function configuracion_inicial(){ webix.ajax().get('/values/main', function(text, data){ var values = data.json() $$('lbl_title_main').setValue(values.empresa) - //~ showvar() + var pos = 4 + if(values.nomina){ + var node = { + id: 'app_nomina', + icon: 'users', + value: 'Nómina'} + $$('main_sidebar').add(node, pos) + pos += 1 + } if(values.punto_de_venta){ var node = { id: 'app_tickets', icon: 'money', value: 'Punto de venta'} - $$('main_sidebar').add(node, 4) + $$('main_sidebar').add(node, pos) } }) @@ -45,6 +53,7 @@ var controllers = { products_controllers.init() bancos_controllers.init() invoices_controllers.init() + nomina_controllers.init() tickets_controllers.init() } } @@ -145,6 +154,14 @@ function multi_change(prevID, nextID){ return } + if(nextID == 'app_nomina'){ + active = $$('multi_nomina').getActiveId() + if(active == 'nomina_home'){ + default_config_nomina() + } + return + } + if(nextID == 'app_invoices'){ active = $$('multi_invoices').getActiveId() if(active == 'invoices_home'){ diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js new file mode 100644 index 0000000..eee5bb0 --- /dev/null +++ b/source/static/js/controller/nomina.js @@ -0,0 +1,52 @@ +var query = [] +var cfg_nomina = new Object() + + +var nomina_controllers = { + init: function(){ + $$('cmd_empleados').attachEvent('onItemClick', cmd_empleados_click) + $$('cmd_close_empleados').attachEvent('onItemClick', cmd_close_empleados_click) + $$('cmd_import_empleados').attachEvent('onItemClick', cmd_import_empleados_click) + webix.extend($$('grid_nomina'), webix.ProgressBar) + } +} + + +function default_config_nomina(){ + +} + + +function current_dates_nomina(){ + var fy = $$('filter_year_nomina') + var fm = $$('filter_month_nomina') + var d = new Date() + + fy.blockEvent() + fm.blockEvent() + + fm.setValue(d.getMonth() + 1) + webix.ajax().sync().get('/values/filteryearsnomina', function(text, data){ + var values = data.json() + fy.getList().parse(values) + fy.setValue(d.getFullYear()) + }) + + fy.unblockEvent() + fm.unblockEvent() +} + + +function cmd_empleados_click(){ + $$('multi_nomina').setValue('nomina_empleados') +} + + +function cmd_close_empleados_click(){ + $$('multi_nomina').setValue('nomina_home') +} + + +function cmd_import_empleados_click(){ + showvar('Importar') +} \ No newline at end of file diff --git a/source/static/js/controller/util.js b/source/static/js/controller/util.js index 0081054..e1f280b 100644 --- a/source/static/js/controller/util.js +++ b/source/static/js/controller/util.js @@ -16,6 +16,7 @@ var table_series = db.addCollection('series') var table_usocfdi = db.addCollection('usocfdi') var table_relaciones = db.addCollection('relaciones') +var msg = '' var months = [ {id: -1, value: 'Todos'}, diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index e5d454f..d679c90 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -78,9 +78,14 @@ var sidebar_admin = { var emisor_datos_fiscales = [ {template: 'Datos SAT', type: 'section'}, - {cols: [{view: 'text', id: 'emisor_rfc', name: 'emisor_rfc', label: 'RFC: ', - width: 300, required: true, invalidMessage: 'RFC inválido', - readonly: true, attributes: {maxlength: 13}}, {}]}, + {cols: [ + {view: 'text', id: 'emisor_rfc', name: 'emisor_rfc', label: 'RFC: ', + width: 300, required: true, invalidMessage: 'RFC inválido', + readonly: true, attributes: {maxlength: 13}}, + {view: 'text', id: 'emisor_curp', name: 'emisor_curp', label: 'CURP: ', + width: 350, labelWidth: 100, attributes: {maxlength: 18}, + placeholder: 'Solo si timbran nómina'}, + {}]}, {view: 'text', id: 'emisor_nombre', name: 'emisor_nombre', label: 'Razón Social: ', required: true, invalidMessage: 'La Razón Social es requerida'}, @@ -116,16 +121,25 @@ var emisor_datos_fiscales = [ var emisor_otros_datos= [ {template: 'Generales', type: 'section'}, - {view: 'search', id: 'emisor_logo', icon: 'file-image-o', - name: 'emisor_logo', label: 'Logotipo: '}, - {view: 'text', id: 'emisor_nombre_comercial', - name: 'emisor_nombre_comercial', label: 'Nombre comercial: '}, - {view: 'text', id: 'emisor_telefono', name: 'emisor_telefono', - label: 'Teléfonos: '}, - {view: 'text', id: 'emisor_correo', name: 'emisor_correo', - label: 'Correos: '}, - {view: 'text', id: 'emisor_web', name: 'emisor_web', - label: 'Página Web: '}, + {cols: [ + {view: 'search', id: 'emisor_logo', icon: 'file-image-o', + name: 'emisor_logo', label: 'Logotipo: '}, + {view: 'text', id: 'emisor_nombre_comercial', + name: 'emisor_nombre_comercial', label: 'Nombre comercial: '}, + ]}, + {cols: [ + {view: 'text', id: 'emisor_telefono', name: 'emisor_telefono', + label: 'Teléfonos: '}, + {view: 'text', id: 'emisor_correo', name: 'emisor_correo', + label: 'Correos: '}, + ]}, + {cols: [ + {view: 'text', id: 'emisor_registro_patronal', attributes: {maxlength: 20}, + name: 'emisor_registro_patronal', label: 'Registro Patronal: ', + placeholder: 'Solo para timbrado de nómina'}, + {view: 'text', id: 'emisor_web', name: 'emisor_web', + label: 'Página Web: '}, + ]}, {template: 'Escuela', type: 'section'}, {cols: [{view: 'checkbox', id: 'chk_escuela', name: 'es_escuela', label: 'Es Escuela'}, @@ -407,7 +421,6 @@ var controls_folios = [ { view: 'tabview', id: 'tab_folios', - //~ tabbar: {options: ['Folios']}, animate: true, cells: [ {id: 'Folios', rows: emisor_folios}, @@ -431,7 +444,6 @@ var controls_correo = [ var form_folios = { type: 'space', - //~ responsive: true, cols: [{ view: 'form', id: 'form_folios', @@ -462,7 +474,6 @@ var form_correo = { labelWidth: 150, labelAlign: 'right' }, - //~ autoheight: true }], } @@ -563,23 +574,25 @@ var options_admin_otros = [ {view: 'checkbox', id: 'chk_ticket_total_up', labelWidth: 0, labelRight: 'Mostrar total arriba'}, {}]}, - + {maxHeight: 20}, + {template: 'Nómina', type: 'section'}, + {cols: [{maxWidth: 15}, + {view: 'checkbox', id: 'chk_usar_nomina', labelWidth: 0, + labelRight: 'Usar timbrado de Nómina'}, + {}]}, {}] - -var body_admin_otros = { - view: 'scrollview', body: {rows: options_admin_otros}, -} - var tab_options = { view: 'tabview', id: 'tab_options', animate: true, cells: [ - {id: 'Plantillas', rows: options_templates}, - {id: 'Otros', rows: options_admin_otros}, - ], + {header: 'Plantillas', body: {id: 'tab_admin_templates', + rows: options_templates}}, + {header: 'Otros', body: {id: 'tab_admin_otros', view: 'scrollview', + body: {rows: options_admin_otros}}}, + ] } @@ -659,7 +672,6 @@ var grid_admin_taxes = { view: 'datatable', id: 'grid_admin_taxes', select: 'cell', - //~ multiselect: true, adjust: true, autoheight: true, autowidth: true, diff --git a/source/static/js/ui/main.js b/source/static/js/ui/main.js index cd6bdaf..d83173f 100644 --- a/source/static/js/ui/main.js +++ b/source/static/js/ui/main.js @@ -2,7 +2,7 @@ var menu_data = [ {id: 'app_home', icon: 'dashboard', value: 'Inicio'}, - {id: 'app_partners', icon: 'users', value: 'Clientes y Proveedores'}, + {id: 'app_partners', icon: 'address-book-o', value: 'Clientes y Proveedores'}, {id: 'app_products', icon: 'server', value: 'Productos y Servicios'}, {id: 'app_bancos', icon: 'university', value: 'Bancos'}, {id: 'app_invoices', icon: 'file-code-o', value: 'Facturas'}, @@ -37,6 +37,7 @@ var multi_main = { app_partners, app_products, app_bancos, + app_nomina, app_tickets, app_invoices, ], diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js new file mode 100644 index 0000000..d665305 --- /dev/null +++ b/source/static/js/ui/nomina.js @@ -0,0 +1,166 @@ + +var toolbar_nomina = [ + {view: 'button', id: 'cmd_empleados', label: 'Empleados', type: 'iconButton', + autowidth: true, icon: 'users'}, + {}, + {view: 'button', id: 'cmd_nomina_report', label: 'Reporte', type: 'iconButton', + autowidth: true, icon: 'table'}, + {}, + {view: 'button', id: 'cmd_nomina_delete', label: 'Eliminar', + type: 'iconButton', autowidth: true, icon: 'minus'}, +] + + +var toolbar_nomina_util = [ + {view: 'button', id: 'cmd_nomina_import', label: 'Importar', + type: 'iconButton', autowidth: true, icon: 'upload'}, + {view: 'button', id: 'cmd_nomina_timbrar', label: 'Timbrar', + type: 'iconButton', autowidth: true, icon: 'ticket'}, + {view: 'button', id: 'cmd_nomina_sat', label: 'SAT', + type: 'iconButton', autowidth: true, icon: 'check-circle'}, + {}, + {view: 'button', id: 'cmd_nomina_cancel', label: 'Cancelar', + type: 'iconButton', autowidth: true, icon: 'ban'}, +] + + +var toolbar_nomina_filter = [ + {view: 'richselect', id: 'filter_year_nomina', label: 'Año', + labelAlign: 'right', labelWidth: 50, width: 150, options: []}, + {view: 'richselect', id: 'filter_month_nomina', label: 'Mes', + labelAlign: 'right', labelWidth: 50, width: 200, options: months}, + {view: 'daterangepicker', id: 'filter_dates_nomina', label: 'Fechas', + labelAlign: 'right', width: 300}, +] + + +var grid_cols_nomina = [ + {id: 'index', header: '#', adjust: 'data', css: 'right', + footer: {content: 'countRows', colspan: 3, css: 'right'}}, + {id: "id", header:"ID", hidden:true}, + {id: "serie", header: ["Serie"], adjust: "header"}, + {id: 'folio', header: ['Folio', {content: 'numberFilter'}], adjust: 'header', + sort: 'int', css: 'right', footer: {text: 'Recibos', colspan: 3}}, + {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: '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: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')}, +] + + +var grid_nomina = { + view: 'datatable', + id: 'grid_nomina', + select: 'row', + multiselect: true, + scrollY: true, + adjust: true, + footer: true, + resizeColumn: true, + headermenu: true, + columns: grid_cols_nomina, + scheme:{ + $change:function(item){ + if (item.estatus == 'Cancelado'){ + item.$css = 'cancel' + } + } + }, + on:{ + 'data->onStoreUpdated':function(){ + this.data.each(function(obj, i){ + obj.index = i + 1 + }) + } + }, +} + + +var rows_nomina_home = [ + {view: 'toolbar', elements: toolbar_nomina}, + {view: 'toolbar', elements: toolbar_nomina_util}, + {view: 'toolbar', elements: toolbar_nomina_filter}, + grid_nomina, +] + + +var toolbar_nomina_empleados = [ + {view: 'button', id: 'cmd_new_empleado', label: 'Nuevo', type: 'iconButton', + autowidth: true, icon: 'user-plus'}, + {view: 'button', id: 'cmd_edit_empleado', label: 'Editar', type: 'iconButton', + autowidth: true, icon: 'user'}, + {view: 'button', id: 'cmd_delete_empleado', label: 'Eliminar', type: 'iconButton', + autowidth: true, icon: 'user-times'}, + {}, + {view: 'button', id: 'cmd_import_empleados', label: 'Importar', + type: 'iconButton', autowidth: true, icon: 'upload'}, + {}, + {view: 'button', id: 'cmd_close_empleados', label: 'Cerrar', type: 'iconButton', + autowidth: true, icon: 'times-circle-o'}, +] + + +var grid_cols_empleados = [ + {id: 'index', header: '#', adjust: 'data', css: 'right', + footer: {content: 'countRows', colspan: 3, css: 'right'}}, + {id: "id", header:"ID", hidden:true}, + {id: "num_empleado", header: ["No Empleado"], adjust: "header"}, + {id: "rfc", header: ["RFC", {content: 'textFilter'}], adjust: "data"}, + {id: "curp", header: ["CURP"], adjust: "data", hidden:true}, + {id: "nombre_completo", header: ["Empleado", {content: 'textFilter'}], + adjust: "data", fillspace: true}, + {id: "fecha_ingreso", header: ["Fecha de Ingreso"], adjust: "data", + sort: "string"}, +] + + +var grid_empleados = { + view: 'datatable', + id: 'grid_empleados', + select: 'row', + scrollY: true, + adjust: true, + footer: true, + resizeColumn: true, + headermenu: true, + columns: grid_cols_empleados, + on:{ + 'data->onStoreUpdated':function(){ + this.data.each(function(obj, i){ + obj.index = i + 1 + }) + } + }, +} + + +var rows_nomina_empleados = [ + {view: 'toolbar', elements: toolbar_nomina_empleados}, + grid_empleados, +] + + +var multi_nomina = { + id: 'multi_nomina', + view: 'multiview', + animate: true, + cells:[ + {id: 'nomina_home', rows: rows_nomina_home}, + {id: 'nomina_empleados', rows: rows_nomina_empleados}, + ], +} + + +var app_nomina = { + id: 'app_nomina', + rows:[ + {view: 'template', type: 'header', template: 'Timbrado de Nómina'}, + multi_nomina + ], +} \ No newline at end of file diff --git a/source/templates/main.html b/source/templates/main.html index d5395ca..9ccb771 100644 --- a/source/templates/main.html +++ b/source/templates/main.html @@ -8,6 +8,7 @@ + @@ -15,6 +16,7 @@ + From 33994b69707d245058f9bfe9ddcbe95ceb236cdc Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 19 Jan 2018 01:00:22 -0600 Subject: [PATCH 03/24] Importar empleados --- source/app/controllers/main.py | 23 +++++++++ source/app/controllers/util.py | 61 +++++++++++++++++++++-- source/app/main.py | 3 +- source/app/models/db.py | 3 ++ source/app/models/main.py | 17 ++++++- source/static/js/controller/nomina.js | 69 ++++++++++++++++++++++++++- source/static/js/ui/nomina.js | 34 ++++++++++++- 7 files changed, 201 insertions(+), 9 deletions(-) diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index a342572..b1b0b1c 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -401,6 +401,29 @@ class AppFolios(object): resp.status = falcon.HTTP_204 +class AppEmployees(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_employees(values) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + values = req.params + req.context['result'] = self._db.employees(values) + resp.status = falcon.HTTP_200 + + def on_delete(self, req, resp): + values = req.params + if self._db.delete('employee', values['id']): + resp.status = falcon.HTTP_200 + else: + resp.status = falcon.HTTP_204 + + class AppDocumentos(object): def __init__(self, db): diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 8ac98bb..144689f 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -969,21 +969,49 @@ class LIBO(object): rows = [dict(zip(fields, r)) for r in data[1:]] return rows, '' - def invoice(self, path): + def employees(self, path): options = {'AsTemplate': True, 'Hidden': True} doc = self._doc_open(path, options) if doc is None: - return (), 'No se pudo abrir la plantilla' + return () - data, msg = self._get_data(doc) + data, msg = self._get_data(doc, 'Empleados') doc.close(True) if len(data) == 1: msg = 'Sin datos para importar' return (), msg - rows = tuple(data[1:]) - return rows, '' + fields = ( + 'num_empleado', + 'rfc', + 'curp', + 'nombre', + 'paterno', + 'materno', + 'fecha_ingreso', + 'imss', + 'tipo_contrato', + 'es_sindicalizado', + 'tipo_jornada', + 'tipo_regimen', + 'departamento', + 'puesto', + 'riesgo_puesto', + 'periodicidad_pago', + 'banco', + 'cuenta_bancaria', + 'clabe', + 'salario_base', + 'salario_diario', + 'estado', + 'codigo_postal', + 'notas', + 'correo', + ) + rows = [dict(zip(fields, r)) for r in data[1:]] + msg = 'Empleados importados correctamente' + return rows, msg def to_pdf(data, emisor_rfc, ods=False): @@ -1014,6 +1042,20 @@ def to_pdf(data, emisor_rfc, ods=False): return read_file(path) +def import_employees(rfc): + name = '{}_employees.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) + if not is_file(path): + return () + + if APP_LIBO: + app = LIBO() + if app.is_running: + return app.employees(path) + + return () + + def parse_xml(xml): return ET.fromstring(xml) @@ -1437,6 +1479,15 @@ def upload_file(rfc, opt, file_obj): name = '{}_invoice.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) + elif opt == 'employees': + tmp = file_obj.filename.split('.') + ext = tmp[-1].lower() + if ext != 'ods': + msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS' + return {'status': 'server', 'name': msg, 'ok': False} + + name = '{}_employees.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) if save_file(path, file_obj.file.read()): return {'status': 'server', 'name': file_obj.filename, 'ok': True} diff --git a/source/app/main.py b/source/app/main.py index 2aa6e39..a394349 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -16,7 +16,7 @@ from controllers.main import (AppEmpresas, AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig, AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios, AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco, - AppMovimientosBanco, AppTickets + AppMovimientosBanco, AppTickets, AppEmployees ) @@ -52,6 +52,7 @@ api.add_route('/preinvoices', AppPreInvoices(db)) api.add_route('/tickets', AppTickets(db)) api.add_route('/cuentasbanco', AppCuentasBanco(db)) api.add_route('/movbanco', AppMovimientosBanco(db)) +api.add_route('/employees', AppEmployees(db)) # ~ Activa si usas waitress y NO estas usando servidor web diff --git a/source/app/models/db.py b/source/app/models/db.py index ad8f47c..77e21c3 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -11,6 +11,9 @@ class StorageEngine(object): def authenticate(self, args): return main.authenticate(args) + def get_employees(self, values): + return main.Empleados.get_by(values) + def empresa_agregar(self, values): return main.empresa_agregar(values['alta_rfc'], False) diff --git a/source/app/models/main.py b/source/app/models/main.py index 5a9ef2d..0a350f5 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -5195,9 +5195,9 @@ class Departamentos(BaseModel): class Puestos(BaseModel): + departamento = ForeignKeyField(Departamentos) nombre = TextField(default='') descripcion = TextField(default='') - departamento = ForeignKeyField(Departamentos) class Meta: order_by = ('nombre',) @@ -5244,6 +5244,21 @@ class Empleados(BaseModel): (('num_empleado', 'rfc'), True), ) + def _import(self): + emisor = Emisor.select()[0] + data, msg = util.import_employees(emisor.rfc) + if not data: + return {'ok': False, 'msg': msg} + + print ('DATA', data) + return {'ok': True, 'msg': msg} + + @classmethod + def get_by(cls, values): + # ~ print (values) + if values['opt'] == 'import': + return cls._import(cls) + class CfdiNomina(BaseModel): empleado = ForeignKeyField(Empleados) diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index eee5bb0..36ed648 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -48,5 +48,72 @@ function cmd_close_empleados_click(){ function cmd_import_empleados_click(){ - showvar('Importar') + win_import_employees.init() + $$('win_import_employees').show() +} + +function cmd_immport_employees_click(){ + var form = $$('form_upload_employees') + + var values = form.getValues() + + if(!$$('lst_upload_employees').count()){ + $$('win_import_employees').close() + return + } + + if($$('lst_upload_employees').count() > 1){ + msg = 'Selecciona solo un archivo' + msg_error(msg) + return + } + + var template = $$('up_employees').files.getItem($$('up_employees').files.getFirstId()) + + if(template.type.toLowerCase() != 'ods'){ + msg = 'Archivo inválido.\n\nSe requiere un archivo ODS' + msg_error(msg) + return + } + + msg = '¿Estás seguro de importar este archivo?' + webix.confirm({ + title: 'Importar Empleados', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('up_employees').send() + } + } + }) +} + + +function up_employees_upload_complete(response){ + if(response.status != 'server'){ + msg = 'Ocurrio un error al subir el archivo' + msg_error(msg) + return + } + msg = 'Archivo subido correctamente.\n\nComenzando importación.' + msg_ok(msg) + $$('win_import_employees').close() + + webix.ajax().get('/employees', {opt: 'import'}, { + error: function(text, data, xhr) { + msg = 'Error al importar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + 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 d665305..e42f37e 100644 --- a/source/static/js/ui/nomina.js +++ b/source/static/js/ui/nomina.js @@ -163,4 +163,36 @@ var app_nomina = { {view: 'template', type: 'header', template: 'Timbrado de Nómina'}, multi_nomina ], -} \ No newline at end of file +} + + +var body_import_employees = {rows: [ + {view: 'form', id: 'form_upload_employees', rows: [ + {cols: [{}, + {view: 'uploader', id: 'up_employees', autosend: false, + link: 'lst_upload_employees', value: 'Seleccionar Plantilla', + upload: '/files/employees'}, {}]}, + {cols: [ + {view: 'list', id: 'lst_upload_employees', name: 'lst_employees', + type: 'uploader', autoheight: true, borderless: true}]}, + {cols: [{}, {view: 'button', id: 'cmd_immport_employees', + label: 'Importar Empleados'}, {}]}, + ]}, +]} + + +var win_import_employees = { + init: function(){ + webix.ui({ + view: 'window', + id: 'win_import_employees', + width: 400, + modal: true, + position: 'center', + head: 'Importar Empleados', + body: body_import_employees, + }) + $$('cmd_immport_employees').attachEvent('onItemClick', cmd_immport_employees_click) + $$('up_employees').attachEvent('onUploadComplete', up_employees_upload_complete) + } +} From 6b24d7819d78f4260664421367aa50f23d340b73 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 23 Jan 2018 23:08:40 -0600 Subject: [PATCH 04/24] Cambios en develop --- source/app/controllers/util.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 144689f..945c3c4 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1013,6 +1013,22 @@ class LIBO(object): msg = 'Empleados importados correctamente' return rows, msg + def invoice(self, path): + options = {'AsTemplate': True, 'Hidden': True} + doc = self._doc_open(path, options) + if doc is None: + return (), 'No se pudo abrir la plantilla' + + data, msg = self._get_data(doc) + doc.close(True) + + if len(data) == 1: + msg = 'Sin datos para importar' + return (), msg + + rows = tuple(data[1:]) + return rows, '' + def to_pdf(data, emisor_rfc, ods=False): rfc = data['emisor']['rfc'] From 6202f9d424805a53a6a9ed6a96975232090480ce Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Wed, 24 Jan 2018 00:51:09 -0600 Subject: [PATCH 05/24] Importar empleados --- source/app/controllers/util.py | 5 +- source/app/models/main.py | 219 ++++++++++++++++++++++++-- source/static/js/controller/nomina.js | 21 ++- source/static/js/ui/nomina.js | 16 +- 4 files changed, 241 insertions(+), 20 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 945c3c4..56218e0 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1009,7 +1009,7 @@ class LIBO(object): 'notas', 'correo', ) - rows = [dict(zip(fields, r)) for r in data[1:]] + rows = tuple([dict(zip(fields, r)) for r in data[1:]]) msg = 'Empleados importados correctamente' return rows, msg @@ -2758,3 +2758,6 @@ def import_invoice(rfc): return (), 'No se encontro LibreOffice' + +def calc_to_date(value): + return datetime.date.fromordinal(int(value) + 693594) diff --git a/source/app/models/main.py b/source/app/models/main.py index 0a350f5..8ef4128 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1400,6 +1400,18 @@ class SATBancos(BaseModel): result = bool(q.execute()) return {'ok': result} + @classmethod + def get_by_key(cls, key): + if not key: + return + try: + obj = SATBancos.get(SATBancos.key==key) + return obj + except SATBancos.DoesNotExist: + msg = 'SATBancos no existe: {}'.format(key) + log.error(msg) + return + class SATNivelesEducativos(BaseModel): name = TextField(index=True) @@ -1867,6 +1879,16 @@ class SATEstados(BaseModel): def __str__(self): return 'Estado: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATEstados.get(SATEstados.key==key) + return obj + except SATEstados.DoesNotExist: + msg = 'SATEstados no existe: {}'.format(key) + log.error(msg) + return + class SATOrigenRecurso(BaseModel): key = TextField(index=True, unique=True) @@ -1882,6 +1904,16 @@ class SATOrigenRecurso(BaseModel): def __str__(self): return 'Origen Recurso: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATOrigenRecurso.get(SATOrigenRecurso.key==key) + return obj + except SATOrigenRecurso.DoesNotExist: + msg = 'SATOrigenRecurso no existe: {}'.format(key) + log.error(msg) + return + class SATPeriodicidadPago(BaseModel): key = TextField(index=True, unique=True) @@ -1897,6 +1929,16 @@ class SATPeriodicidadPago(BaseModel): def __str__(self): return 'Periodicidad de Pago: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATPeriodicidadPago.get(SATPeriodicidadPago.key==key) + return obj + except SATPeriodicidadPago.DoesNotExist: + msg = 'SATPeriodicidadPago no existe: {}'.format(key) + log.error(msg) + return + class SATTipoContrato(BaseModel): key = TextField(index=True, unique=True) @@ -1912,6 +1954,16 @@ class SATTipoContrato(BaseModel): def __str__(self): return 'Tipo de Contrato: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoContrato.get(SATTipoContrato.key==key) + return obj + except SATTipoContrato.DoesNotExist: + msg = 'SATTipoContrato no existe: {}'.format(key) + log.error(msg) + return + class SATTipoDeduccion(BaseModel): key = TextField(index=True, unique=True) @@ -1929,6 +1981,16 @@ class SATTipoDeduccion(BaseModel): def __str__(self): return 'Tipo de Deducción: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoDeduccion.get(SATTipoDeduccion.key==key) + return obj + except SATTipoDeduccion.DoesNotExist: + msg = 'SATTipoDeduccion no existe: {}'.format(key) + log.error(msg) + return + class SATTipoHoras(BaseModel): key = TextField(index=True, unique=True) @@ -1944,6 +2006,16 @@ class SATTipoHoras(BaseModel): def __str__(self): return 'Tipo de Horas: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoHoras.get(SATTipoHoras.key==key) + return obj + except SATTipoHoras.DoesNotExist: + msg = 'SATTipoHoras no existe: {}'.format(key) + log.error(msg) + return + class SATTipoIncapacidad(BaseModel): key = TextField(index=True, unique=True) @@ -1959,6 +2031,16 @@ class SATTipoIncapacidad(BaseModel): def __str__(self): return 'Tipo de Incapacidad: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoIncapacidad.get(SATTipoIncapacidad.key==key) + return obj + except SATTipoIncapacidad.DoesNotExist: + msg = 'SATTipoIncapacidad no existe: {}'.format(key) + log.error(msg) + return + class SATTipoJornada(BaseModel): key = TextField(index=True, unique=True) @@ -1974,6 +2056,16 @@ class SATTipoJornada(BaseModel): def __str__(self): return 'Tipo de Jornada: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoJornada.get(SATTipoJornada.key==key) + return obj + except SATTipoJornada.DoesNotExist: + msg = 'SATTipoJornada no existe: {}'.format(key) + log.error(msg) + return + class SATTipoNomina(BaseModel): key = TextField(index=True, unique=True) @@ -1989,6 +2081,16 @@ class SATTipoNomina(BaseModel): def __str__(self): return 'Tipo de Nómina: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoNomina.get(SATTipoNomina.key==key) + return obj + except SATTipoNomina.DoesNotExist: + msg = 'SATTipoNomina no existe: {}'.format(key) + log.error(msg) + return + class SATTipoOtroPago(BaseModel): key = TextField(index=True, unique=True) @@ -2006,6 +2108,16 @@ class SATTipoOtroPago(BaseModel): def __str__(self): return 'Tipo de Otro Pago: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoOtroPago.get(SATTipoOtroPago.key==key) + return obj + except SATTipoOtroPago.DoesNotExist: + msg = 'SATTipoOtroPago no existe: {}'.format(key) + log.error(msg) + return + class SATTipoPercepcion(BaseModel): key = TextField(index=True, unique=True) @@ -2023,6 +2135,16 @@ class SATTipoPercepcion(BaseModel): def __str__(self): return 'Tipo de Percepción: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoPercepcion.get(SATTipoPercepcion.key==key) + return obj + except SATTipoPercepcion.DoesNotExist: + msg = 'SATTipoPercepcion no existe: {}'.format(key) + log.error(msg) + return + class SATTipoRegimen(BaseModel): key = TextField(index=True, unique=True) @@ -2038,6 +2160,16 @@ class SATTipoRegimen(BaseModel): def __str__(self): return 'Regimen de contratación: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATTipoRegimen.get(SATTipoRegimen.key==key) + return obj + except SATTipoRegimen.DoesNotExist: + msg = 'SATTipoRegimen no existe: {}'.format(key) + log.error(msg) + return + class SATRiesgoPuesto(BaseModel): key = TextField(index=True, unique=True) @@ -2053,6 +2185,16 @@ class SATRiesgoPuesto(BaseModel): def __str__(self): return 'Riesgo Puesto: {} ({})'.format(self.name, self.key) + @classmethod + def get_by_key(cls, key): + try: + obj = SATRiesgoPuesto.get(SATRiesgoPuesto.key==key) + return obj + except SATRiesgoPuesto.DoesNotExist: + msg = 'SATRiesgoPuesto no existe: {}'.format(key) + log.error(msg) + return + class TipoCambio(BaseModel): dia = DateField(default=util.now) @@ -5187,7 +5329,7 @@ class SeriesProductos(BaseModel): class Departamentos(BaseModel): - nombre = TextField(default='') + nombre = TextField(default='', unique=True) descripcion = TextField(default='') class Meta: @@ -5195,15 +5337,22 @@ class Departamentos(BaseModel): class Puestos(BaseModel): - departamento = ForeignKeyField(Departamentos) - nombre = TextField(default='') + departamento = ForeignKeyField(Departamentos, null=True) + nombre = TextField(default='', unique=True) descripcion = TextField(default='') class Meta: order_by = ('nombre',) - indexes = ( - (('nombre', 'departamento'), True), - ) + + @classmethod + def get_by_depto(cls, puesto, depto): + departamento = None + if depto: + with database_proxy.transaction(): + departamento, _ = Departamentos.get_or_create(nombre=depto) + data = {'departamento': departamento, 'nombre': puesto} + obj, _ = Puestos.get_or_create(**data) + return obj class Empleados(BaseModel): @@ -5244,18 +5393,68 @@ class Empleados(BaseModel): (('num_empleado', 'rfc'), True), ) + def _validate_import(self, row): + sn = {'si': True, 'no': False} + data = row.copy() + data['nombre_completo'] = '{} {} {}'.format( + row['nombre'], row['paterno'], row['materno']).strip() + 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']) + data['tipo_regimen'] = SATTipoRegimen.get_by_key(row['tipo_regimen']) + data['puesto'] = Puestos.get_by_depto(row['puesto'], row['departamento']) + data['riesgo_puesto'] = SATRiesgoPuesto.get_by_key(row['riesgo_puesto']) + data['periodicidad_pago'] = SATPeriodicidadPago.get_by_key(row['periodicidad_pago']) + data['banco'] = SATBancos.get_by_key(row['banco']) + data['estado'] = SATEstados.get_by_key(row['estado']) + del data['departamento'] + return data + def _import(self): emisor = Emisor.select()[0] - data, msg = util.import_employees(emisor.rfc) - if not data: + rows, msg = util.import_employees(emisor.rfc) + if not rows: return {'ok': False, 'msg': msg} - print ('DATA', data) + en = 0 + ea = 0 + for row in rows: + data = self._validate_import(self, row) + w = (Empleados.rfc==row['rfc']) + with database_proxy.transaction(): + if Empleados.select().where(w).exists(): + q = Empleados.update(**data).where(w) + q.execute() + ea += 1 + else: + obj = Empleados.create(**data) + en += 1 + + msg = 'Empleados encontrados: {}
'.format(len(rows)) + msg += 'Empleados nuevos: {}
'.format(en) + msg += 'Empleados actualizados: {}
'.format(ea) + msg += 'Empleados no importados: {}'.format(len(rows) - en - ea) return {'ok': True, 'msg': msg} + def _get(self): + rows = (Empleados + .select( + Empleados.id, + Empleados.num_empleado, + Empleados.rfc, + Empleados.curp, + Empleados.nombre_completo, + Empleados.fecha_ingreso) + .dicts() + ) + return {'ok': True, 'rows': tuple(rows)} + @classmethod def get_by(cls, values): - # ~ print (values) + if not 'opt' in values: + return cls._get(cls) + if values['opt'] == 'import': return cls._import(cls) diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 36ed648..033bd8e 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -36,8 +36,27 @@ function current_dates_nomina(){ fm.unblockEvent() } +function get_employees(){ + webix.ajax().get('/employees', { + 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_employees').clearAll(); + $$('grid_employees').parse(values.rows, 'json'); + }else{ + msg_error(values.msg) + } + } + }) +} + function cmd_empleados_click(){ + get_employees() $$('multi_nomina').setValue('nomina_empleados') } @@ -52,7 +71,7 @@ function cmd_import_empleados_click(){ $$('win_import_employees').show() } -function cmd_immport_employees_click(){ +function cmd_import_employees_click(){ var form = $$('form_upload_employees') var values = form.getValues() diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js index e42f37e..c5b3d39 100644 --- a/source/static/js/ui/nomina.js +++ b/source/static/js/ui/nomina.js @@ -111,18 +111,18 @@ var grid_cols_empleados = [ footer: {content: 'countRows', colspan: 3, css: 'right'}}, {id: "id", header:"ID", hidden:true}, {id: "num_empleado", header: ["No Empleado"], adjust: "header"}, - {id: "rfc", header: ["RFC", {content: 'textFilter'}], adjust: "data"}, - {id: "curp", header: ["CURP"], adjust: "data", hidden:true}, + {id: "rfc", header: ["RFC", {content: 'textFilter'}], adjust: "data", sort: "string"}, + {id: "curp", header: ["CURP"], adjust: "data", hidden:true, sort: "string"}, {id: "nombre_completo", header: ["Empleado", {content: 'textFilter'}], - adjust: "data", fillspace: true}, - {id: "fecha_ingreso", header: ["Fecha de Ingreso"], adjust: "data", + adjust: "data", fillspace: true, sort: "string"}, + {id: "fecha_ingreso", header: ["Fecha de Ingreso"], adjust: "header", sort: "string"}, ] var grid_empleados = { view: 'datatable', - id: 'grid_empleados', + id: 'grid_employees', select: 'row', scrollY: true, adjust: true, @@ -175,10 +175,10 @@ var body_import_employees = {rows: [ {cols: [ {view: 'list', id: 'lst_upload_employees', name: 'lst_employees', type: 'uploader', autoheight: true, borderless: true}]}, - {cols: [{}, {view: 'button', id: 'cmd_immport_employees', + {cols: [{}, {view: 'button', id: 'cmd_import_employees', label: 'Importar Empleados'}, {}]}, ]}, -]} +],} var win_import_employees = { @@ -192,7 +192,7 @@ var win_import_employees = { head: 'Importar Empleados', body: body_import_employees, }) - $$('cmd_immport_employees').attachEvent('onItemClick', cmd_immport_employees_click) + $$('cmd_import_employees').attachEvent('onItemClick', cmd_import_employees_click) $$('up_employees').attachEvent('onUploadComplete', up_employees_upload_complete) } } From d2f4d7c49dc5433ab2e43c3ceec6cbbf392c5533 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Wed, 24 Jan 2018 01:03:44 -0600 Subject: [PATCH 06/24] Actualizar empleados --- source/app/models/main.py | 3 +++ source/static/js/controller/nomina.js | 1 + source/static/js/ui/nomina.js | 7 ++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index 8ef4128..dce1ad2 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -5445,6 +5445,9 @@ class Empleados(BaseModel): Empleados.rfc, Empleados.curp, Empleados.nombre_completo, + Empleados.imss, + Empleados.salario_base, + Empleados.salario_diario, Empleados.fecha_ingreso) .dicts() ) diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 033bd8e..5ef52d0 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -130,6 +130,7 @@ function up_employees_upload_complete(response){ var values = data.json(); if (values.ok){ msg_ok(values.msg) + get_employees() }else{ msg_error(values.msg) } diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js index c5b3d39..1f82cdb 100644 --- a/source/static/js/ui/nomina.js +++ b/source/static/js/ui/nomina.js @@ -110,11 +110,16 @@ var grid_cols_empleados = [ {id: 'index', header: '#', adjust: 'data', css: 'right', footer: {content: 'countRows', colspan: 3, css: 'right'}}, {id: "id", header:"ID", hidden:true}, - {id: "num_empleado", header: ["No Empleado"], adjust: "header"}, + {id: "num_empleado", header: ["No Empleado"], adjust: "data"}, {id: "rfc", header: ["RFC", {content: 'textFilter'}], adjust: "data", sort: "string"}, {id: "curp", header: ["CURP"], adjust: "data", hidden:true, sort: "string"}, {id: "nombre_completo", header: ["Empleado", {content: 'textFilter'}], adjust: "data", fillspace: true, sort: "string"}, + {id: 'imss', header: ['IMSS'], adjust: 'data'}, + {id: 'salario_base', header: ['Salario Base'], adjust: 'header', + format: webix.i18n.priceFormat, css: 'right'}, + {id: 'salario_diario', header: ['Salario Diario'], adjust: 'header', + format: webix.i18n.priceFormat, css: 'right'}, {id: "fecha_ingreso", header: ["Fecha de Ingreso"], adjust: "header", sort: "string"}, ] From 1155b96eebdd7e6c8d92a0a2dc3850a423facd56 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Wed, 24 Jan 2018 22:37:28 -0600 Subject: [PATCH 07/24] Eliminar empleado --- source/app/models/db.py | 2 ++ source/app/models/main.py | 5 ++++ source/static/js/controller/nomina.js | 42 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/source/app/models/db.py b/source/app/models/db.py index 77e21c3..a1557aa 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -247,6 +247,8 @@ class StorageEngine(object): return main.Usuarios.remove(id) if table == 'config': return main.Configuracion.remove(id) + if table == 'employee': + return main.Empleados.remove(id) return False def _get_client(self, values): diff --git a/source/app/models/main.py b/source/app/models/main.py index dce1ad2..ef14780 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -5461,6 +5461,11 @@ class Empleados(BaseModel): if values['opt'] == 'import': return cls._import(cls) + @classmethod + def remove(cls, id): + q = Empleados.delete().where(Empleados.id==id) + return bool(q.execute()) + class CfdiNomina(BaseModel): empleado = ForeignKeyField(Empleados) diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 5ef52d0..7976875 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -6,6 +6,7 @@ var nomina_controllers = { init: function(){ $$('cmd_empleados').attachEvent('onItemClick', cmd_empleados_click) $$('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) webix.extend($$('grid_nomina'), webix.ProgressBar) } @@ -136,4 +137,45 @@ function up_employees_upload_complete(response){ } } }) +} + + +function delete_empleado(id){ + webix.ajax().del('/employees', {id: id}, function(text, xml, xhr){ + var msg = 'Empleado eliminado correctamente' + if (xhr.status == 200){ + $$('grid_employees').remove(id); + msg_ok(msg) + } else { + msg = 'No se pudo eliminar.' + msg_error(msg) + } + }) +} + + +function cmd_delete_empleado_click(){ + var row = $$('grid_employees').getSelectedItem() + + if (row == undefined){ + msg = 'Selecciona un Empleado' + msg_error(msg) + return + } + + msg = '¿Estás seguro de eliminar al Empleado?

' + msg += row['nombre_completo'] + ' (' + row['rfc'] + ')' + msg += '

ESTA ACCIÓN NO SE PUEDE DESHACER

' + webix.confirm({ + title: 'Eliminar Empleado', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if (result){ + delete_empleado(row['id']) + } + } + }) } \ No newline at end of file From b4cab5aa2fe9e73a7613649984bcc9f9c2d4c672 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 25 Jan 2018 21:36:37 -0600 Subject: [PATCH 08/24] Add button for import nomina --- source/static/js/controller/nomina.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 7976875..1b57859 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -4,6 +4,7 @@ var cfg_nomina = new Object() var nomina_controllers = { init: function(){ + $$('cmd_nomina_import').attachEvent('onItemClick', cmd_nomina_import_click) $$('cmd_empleados').attachEvent('onItemClick', cmd_empleados_click) $$('cmd_close_empleados').attachEvent('onItemClick', cmd_close_empleados_click) $$('cmd_delete_empleado').attachEvent('onItemClick', cmd_delete_empleado_click) @@ -37,6 +38,12 @@ function current_dates_nomina(){ fm.unblockEvent() } + +function cmd_nomina_import_click(){ + msg_ok('ok') +} + + function get_employees(){ webix.ajax().get('/employees', { error: function(text, data, xhr) { From cd8bd1ceeb3c5d629725ce7c6a04dd508cde459f Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 26 Jan 2018 01:52:59 -0600 Subject: [PATCH 09/24] Import nomina from template --- source/app/controllers/main.py | 23 ++++++++ source/app/controllers/util.py | 44 ++++++++++++++++ source/app/main.py | 3 +- source/app/models/db.py | 3 ++ source/app/models/main.py | 52 +++++++++++++++++- source/static/js/controller/nomina.js | 76 ++++++++++++++++++++++++++- source/static/js/ui/nomina.js | 32 +++++++++++ 7 files changed, 229 insertions(+), 4 deletions(-) diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index b1b0b1c..946c137 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -424,6 +424,29 @@ class AppEmployees(object): resp.status = falcon.HTTP_204 +class AppNomina(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_nomina(values) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + values = req.params + req.context['result'] = self._db.nomina(values) + resp.status = falcon.HTTP_200 + + def on_delete(self, req, resp): + values = req.params + if self._db.delete('nomina', values): + resp.status = falcon.HTTP_200 + else: + resp.status = falcon.HTTP_204 + + class AppDocumentos(object): def __init__(self, db): diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 56218e0..e80941a 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1013,6 +1013,27 @@ class LIBO(object): msg = 'Empleados importados correctamente' return rows, msg + def nomina(self, path): + options = {'AsTemplate': True, 'Hidden': True} + doc = self._doc_open(path, options) + if doc is None: + return () + + data, msg = self._get_data(doc, 'Nomina') + doc.close(True) + + if len(data) == 1: + msg = 'Sin datos para importar' + return (), msg + + fields = ( + 'num_empleado', + 'rfc', + ) + rows = tuple([dict(zip(fields, r)) for r in data[1:]]) + msg = 'Nomina importada correctamente' + return rows, msg + def invoice(self, path): options = {'AsTemplate': True, 'Hidden': True} doc = self._doc_open(path, options) @@ -1072,6 +1093,20 @@ def import_employees(rfc): return () +def import_nomina(rfc): + name = '{}_nomina.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) + if not is_file(path): + return () + + if APP_LIBO: + app = LIBO() + if app.is_running: + return app.nomina(path) + + return () + + def parse_xml(xml): return ET.fromstring(xml) @@ -1504,6 +1539,15 @@ def upload_file(rfc, opt, file_obj): name = '{}_employees.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) + elif opt == 'nomina': + tmp = file_obj.filename.split('.') + ext = tmp[-1].lower() + if ext != 'ods': + msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS' + return {'status': 'server', 'name': msg, 'ok': False} + + name = '{}_nomina.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) if save_file(path, file_obj.file.read()): return {'status': 'server', 'name': file_obj.filename, 'ok': True} diff --git a/source/app/main.py b/source/app/main.py index a394349..a11dfd4 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -16,7 +16,7 @@ from controllers.main import (AppEmpresas, AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig, AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios, AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco, - AppMovimientosBanco, AppTickets, AppEmployees + AppMovimientosBanco, AppTickets, AppEmployees, AppNomina ) @@ -53,6 +53,7 @@ api.add_route('/tickets', AppTickets(db)) api.add_route('/cuentasbanco', AppCuentasBanco(db)) api.add_route('/movbanco', AppMovimientosBanco(db)) api.add_route('/employees', AppEmployees(db)) +api.add_route('/nomina', AppNomina(db)) # ~ Activa si usas waitress y NO estas usando servidor web diff --git a/source/app/models/db.py b/source/app/models/db.py index a1557aa..4d9e420 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -14,6 +14,9 @@ class StorageEngine(object): def get_employees(self, values): return main.Empleados.get_by(values) + def get_nomina(self, values): + return main.CfdiNomina.get_by(values) + def empresa_agregar(self, values): return main.empresa_agregar(values['alta_rfc'], False) diff --git a/source/app/models/main.py b/source/app/models/main.py index ef14780..3d2a68a 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -67,7 +67,8 @@ def desconectar(): def upload_file(rfc, opt, file_obj): result = util.upload_file(rfc, opt, file_obj) if result['ok']: - if opt != 'bdfl': + names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods') + if not opt in names: Configuracion.add({opt: file_obj.filename}) return result @@ -197,7 +198,7 @@ def config_main(): nomina = util.get_bool(Configuracion.get_('chk_usar_nomina')) data = { 'empresa': get_title_app(3), - 'punto_de_venta': punto_de_venta + 'punto_de_venta': punto_de_venta, 'nomina': nomina, } if not obj is None: @@ -5526,6 +5527,53 @@ class CfdiNomina(BaseModel): class Meta: order_by = ('fecha',) + def _validate_import(self, row): + sn = {'si': True, 'no': False} + data = row.copy() + return data + + def _import(self): + emisor = Emisor.select()[0] + rows, msg = util.import_nomina(emisor.rfc) + if not rows: + return {'ok': False, 'msg': msg} + + for row in rows: + data = self._validate_import(self, row) + # ~ w = (Nomina.empleado.rfc==row['rfc']) + with database_proxy.transaction(): + pass + # ~ if Empleados.select().where(w).exists(): + # ~ q = Empleados.update(**data).where(w) + # ~ q.execute() + # ~ else: + # ~ obj = Empleados.create(**data) + return {'ok': True, 'msg': msg} + + def _get(self): + rows = (nomina + .select( + Nomina.id, + Nomina.serie, + Nomina.folio, + Nomina.fecha, + Nomina.status, + Nomina.fecha_pago, + Nomina.total, + Nomina.empleado.nombre_completo + ) + .dicts() + ) + return {'ok': True, 'rows': tuple(rows)} + + @classmethod + def get_by(cls, values): + if not 'opt' in values: + return cls._get(cls) + + if values['opt'] == 'import': + return cls._import(cls) + class CfdiNominaDetalle(BaseModel): cfdi = ForeignKeyField(CfdiNomina) diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 1b57859..c09668a 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -39,8 +39,82 @@ function current_dates_nomina(){ } +function get_nomina(){ + +} + + function cmd_nomina_import_click(){ - msg_ok('ok') + win_import_nomina.init() + $$('win_import_nomina').show() +} + + +function cmd_import_template_nomina_click(){ + var form = $$('form_upload_nomina') + + var values = form.getValues() + + if(!$$('lst_upload_nomina').count()){ + $$('win_import_nomina').close() + return + } + + if($$('lst_upload_nomina').count() > 1){ + msg = 'Selecciona solo un archivo' + msg_error(msg) + return + } + + var template = $$('up_nomina').files.getItem($$('up_nomina').files.getFirstId()) + + if(template.type.toLowerCase() != 'ods'){ + msg = 'Archivo inválido.\n\nSe requiere un archivo ODS' + msg_error(msg) + return + } + + msg = '¿Estás seguro de importar este archivo?' + webix.confirm({ + title: 'Importar Nómina', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('up_nomina').send() + } + } + }) +} + + +function up_nomina_upload_complete(response){ + if(response.status != 'server'){ + msg = 'Ocurrio un error al subir el archivo' + msg_error(msg) + return + } + msg = 'Archivo subido correctamente.\n\nComenzando importación.' + msg_ok(msg) + $$('win_import_nomina').close() + + webix.ajax().get('/nomina', {opt: 'import'}, { + error: function(text, data, xhr) { + msg = 'Error al importar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + msg_ok(values.msg) + get_nomina() + }else{ + msg_error(values.msg) + } + } + }) } diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js index 1f82cdb..5cbe1a2 100644 --- a/source/static/js/ui/nomina.js +++ b/source/static/js/ui/nomina.js @@ -201,3 +201,35 @@ var win_import_employees = { $$('up_employees').attachEvent('onUploadComplete', up_employees_upload_complete) } } + + +var body_import_nomina = {rows: [ + {view: 'form', id: 'form_upload_nomina', rows: [ + {cols: [{}, + {view: 'uploader', id: 'up_nomina', autosend: false, + link: 'lst_upload_nomina', value: 'Seleccionar Plantilla', + upload: '/files/nomina'}, {}]}, + {cols: [ + {view: 'list', id: 'lst_upload_nomina', name: 'lst_nomina', + type: 'uploader', autoheight: true, borderless: true}]}, + {cols: [{}, {view: 'button', id: 'cmd_import_template_nomina', + label: 'Importar Nómina'}, {}]}, + ]}, +],} + + +var win_import_nomina = { + init: function(){ + webix.ui({ + view: 'window', + id: 'win_import_nomina', + width: 400, + modal: true, + position: 'center', + head: 'Importar Nómina', + body: body_import_nomina, + }) + $$('cmd_import_template_nomina').attachEvent('onItemClick', cmd_import_template_nomina_click) + $$('up_nomina').attachEvent('onUploadComplete', up_nomina_upload_complete) + } +} From 776c90a4673ec39246a7a1a9a974701b84504b5d Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sat, 27 Jan 2018 02:45:45 -0600 Subject: [PATCH 10/24] =?UTF-8?q?Refactorizar=20generaci=C3=B3n=20de=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/controllers/util.py | 57 ++++++++++++++++++++---- source/static/js/controller/invoices.js | 3 +- source/static/js/ui/products.js | 5 ++- source/templates/plantilla_factura.ods | Bin 27358 -> 26044 bytes 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index e80941a..de8c97d 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -760,8 +760,29 @@ class LIBO(object): self._sheet.copyRange(nc.getCellAddress(), source.getRangeAddress()) return + def _copy_paste_rows(self, cell, count): + dispatch = self._create_instance('com.sun.star.frame.DispatchHelper') + + row = cell.getCellAddress().Row + source = self._sheet.getRows().getByIndex(row) + self._template.getCurrentController().select(source) + frame = self._template.getCurrentController().getFrame() + dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ()) + + target = self._sheet.getCellRangeByPosition(0, row + 1, 0, row + count) + self._template.getCurrentController().select(target) + dispatch.executeDispatch(frame, '.uno:Paste', '', 0, ()) + return + def _conceptos(self, data): first = True + col1 = [] + col2 = [] + col3 = [] + col4 = [] + col5 = [] + col6 = [] + count = len(data)-1 for concepto in data: key = concepto.get('noidentificacion', '') description = concepto['descripcion'] @@ -779,16 +800,36 @@ class LIBO(object): cell_6 = self._set_cell('{importe}', importe, value=True) if len(data) > 1: row = cell_1.getCellAddress().Row + 1 - self._sheet.getRows().insertByIndex(row, len(data)-1) + self._sheet.getRows().insertByIndex(row, count) + self._copy_paste_rows(cell_1, count) + row = cell_1.getCellAddress().Row else: - self._copy_row(cell_1) - cell_1 = self._set_cell(v=key, cell=cell_1) - cell_2 = self._set_cell(v=description, cell=cell_2) - cell_3 = self._set_cell(v=unidad, cell=cell_3) - cell_4 = self._set_cell(v=cantidad, cell=cell_4, value=True) - cell_5 = self._set_cell(v=valor_unitario, cell=cell_5, value=True) - cell_6 = self._set_cell(v=importe, cell=cell_6, value=True) + col1.append((key,)) + col2.append((description,)) + col3.append((unidad,)) + col4.append((float(cantidad),)) + col5.append((float(valor_unitario),)) + col6.append((float(importe),)) + col = cell_1.getCellAddress().Column + target1 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_2.getCellAddress().Column + target2 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_3.getCellAddress().Column + target3 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_4.getCellAddress().Column + target4 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_5.getCellAddress().Column + target5 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + col = cell_6.getCellAddress().Column + target6 = self._sheet.getCellRangeByPosition(col, row+1, col, row+count) + + target1.setFormulaArray(tuple(col1)) + target2.setDataArray(tuple(col2)) + target3.setFormulaArray(tuple(col3)) + target4.setDataArray(tuple(col4)) + target5.setDataArray(tuple(col5)) + target6.setDataArray(tuple(col6)) return def _add_totales(self, data): diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 2bc88d4..f1f463c 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -1592,7 +1592,8 @@ function generate_pdf(id){ function grid_preinvoices_click(id, e, node){ if(id.column == 'pdf'){ - generate_pdf(id.row) + //~ generate_pdf(id.row) + window.open('/doc/pre/' + id, '_blank') }else if(id.column == 'email'){ enviar_prefactura(id) } diff --git a/source/static/js/ui/products.js b/source/static/js/ui/products.js index d83904e..72dd511 100644 --- a/source/static/js/ui/products.js +++ b/source/static/js/ui/products.js @@ -209,9 +209,10 @@ var multi_products = { var app_products = { - id: "app_products", + id: 'app_products', rows:[ - {view: "template", id: "th_products", type: "header", template:"Administración de Productos" }, + {view: 'template', id: 'th_products', type: 'header', + template: 'Administración de Productos y Servicios'}, multi_products ], } diff --git a/source/templates/plantilla_factura.ods b/source/templates/plantilla_factura.ods index 1f332ceafda4f596c5c344a2159affc681322115..c66d20f5cc9d4f24be4aa5267ea9c5726c51efe1 100644 GIT binary patch delta 17911 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%0D6^Rc#)H8keGx2rEuUl15_@NAk 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%0A;kjhhD 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 SG8pP14;K1J|F+v_wdMa1#nOoY From 6a66b15f56f6042371bb7695dbd116cb0ab22352 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 28 Jan 2018 03:12:35 -0600 Subject: [PATCH 11/24] =?UTF-8?q?Importar=20n=C3=B3mina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/controllers/util.py | 143 +++++++++++++-- source/app/models/main.py | 314 +++++++++++++++++++++++++++++++-- source/app/settings.py | 3 + 3 files changed, 431 insertions(+), 29 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index de8c97d..eb871c5 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1054,26 +1054,126 @@ class LIBO(object): msg = 'Empleados importados correctamente' return rows, msg + def _get_nomina(self, doc): + rows, msg = self._get_data(doc, 'Nomina') + if len(rows) == 2: + msg = 'Sin datos para importar' + return {}, msg + + fields = ( + 'rfc', + 'tipo_nomina', + 'fecha_pago', + 'fecha_inicial_pago', + 'fecha_final_pago', + ) + data = tuple([dict(zip(fields, r[1:])) for r in rows[2:]]) + return data, '' + + def _get_percepciones(self, doc, count): + rows, msg = self._get_data(doc, 'Percepciones') + if len(rows) == 2: + msg = 'Sin Percepciones' + return {}, msg + + if len(rows[0][2:]) % 2: + msg = 'Las Percepciones deben ir en pares: Gravado y Exento' + return {}, msg + + data = tuple([r[2:] for r in rows[:count+2]]) + return data, '' + + def _get_deducciones(self, doc, count): + rows, msg = self._get_data(doc, 'Deducciones') + if len(rows) == 2: + msg = 'Sin Deducciones' + return {}, msg + + data = tuple([r[2:] for r in rows[:count+2]]) + return data, '' + + def _get_otros_pagos(self, doc, count): + rows, msg = self._get_data(doc, 'OtrosPagos') + if len(rows) == 2: + msg = 'Sin Otros Pagos' + return {}, msg + + data = tuple([r[2:] for r in rows[:count+2]]) + return data, '' + + def _get_horas_extras(self, doc, count): + rows, msg = self._get_data(doc, 'HorasExtras') + if len(rows) == 2: + msg = 'Sin Horas Extras' + return {}, msg + + if len(rows[1][1:]) % 4: + msg = 'Las Horas Extras deben ir grupos de 4 columnas' + return {}, msg + + data = tuple([r[1:] for r in rows[:count+2]]) + return data, '' + + def _get_incapacidades(self, doc, count): + rows, msg = self._get_data(doc, 'Incapacidades') + if len(rows) == 2: + msg = 'Sin Incapacidades' + return {}, msg + + if len(rows[1][1:]) % 3: + msg = 'Las Incapacidades deben ir grupos de 3 columnas' + return {}, msg + + data = tuple([r[1:] for r in rows[:count+2]]) + return data, '' + def nomina(self, path): options = {'AsTemplate': True, 'Hidden': True} doc = self._doc_open(path, options) if doc is None: - return () + msg = 'No se pudo abrir la plantilla' + return {}, msg + + data = {} + nomina, msg = self._get_nomina(doc) + if msg: + doc.close(True) + return {}, msg + + percepciones, msg = self._get_percepciones(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg + + deducciones, msg = self._get_deducciones(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg + + otros_pagos, msg = self._get_otros_pagos(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg + + horas_extras, msg = self._get_horas_extras(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg + + incapacidades, msg = self._get_incapacidades(doc, len(nomina)) + if msg: + doc.close(True) + return {}, msg - data, msg = self._get_data(doc, 'Nomina') doc.close(True) + data['nomina'] = nomina + data['percepciones'] = percepciones + data['deducciones'] = deducciones + data['otros_pagos'] = otros_pagos + data['horas_extras'] = horas_extras + data['incapacidades'] = incapacidades - if len(data) == 1: - msg = 'Sin datos para importar' - return (), msg - - fields = ( - 'num_empleado', - 'rfc', - ) - rows = tuple([dict(zip(fields, r)) for r in data[1:]]) - msg = 'Nomina importada correctamente' - return rows, msg + return data, '' def invoice(self, path): options = {'AsTemplate': True, 'Hidden': True} @@ -2846,3 +2946,20 @@ def import_invoice(rfc): def calc_to_date(value): return datetime.date.fromordinal(int(value) + 693594) + + +def get_days(start, end): + return (end - start).days + 1 + + +def log_file(name, msg='', kill=False): + path = _join(PATH_MEDIA, 'tmp', '{}.log'.format(name)) + + if kill: + _kill(path) + return + + with open(path, 'a') as fh: + line = '{} : {}'.format(str(now()), msg) + fh.write(line) + return diff --git a/source/app/models/main.py b/source/app/models/main.py index 3d2a68a..5ee418d 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -5421,6 +5421,8 @@ class Empleados(BaseModel): en = 0 ea = 0 for row in rows: + # ~ if row['rfc'] == 'BASM740115RW0': + # ~ continue data = self._validate_import(self, row) w = (Empleados.rfc==row['rfc']) with database_proxy.transaction(): @@ -5471,7 +5473,7 @@ class Empleados(BaseModel): class CfdiNomina(BaseModel): empleado = ForeignKeyField(Empleados) version = TextField(default=CURRENT_CFDI) - serie = TextField(default='') + serie = TextField(default='N') folio = IntegerField(default=0) fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) fecha_timbrado = DateTimeField(null=True) @@ -5488,7 +5490,7 @@ class CfdiNomina(BaseModel): auto_round=True) total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, auto_round=True) - tipo_comprobante = TextField(default='I') + tipo_comprobante = TextField(default='N') metodo_pago = TextField(default='PUE') lugar_expedicion = TextField(default='') confirmacion = TextField(default='') @@ -5511,7 +5513,7 @@ class CfdiNomina(BaseModel): acuse = TextField(default='') tipo_relacion = TextField(default='') error = TextField(default='') - version = TextField(default=CURRENT_CFDI_NOMINA) + version_nomina = TextField(default=CURRENT_CFDI_NOMINA) registro_patronal = TextField(default='') rfc_patron_origen = TextField(default='') tipo_nomina = ForeignKeyField(SATTipoNomina) @@ -5527,27 +5529,304 @@ class CfdiNomina(BaseModel): class Meta: order_by = ('fecha',) - def _validate_import(self, row): + def _get_serie(self): + serie = Configuracion.get_('chk_config_serie_nomina') + if not serie: + serie = DEFAULT_SAT_NOMINA['SERIE'] + return serie + + def _get_folio(self, serie): + folio = int(Configuracion.get_('chk_config_folio_nomina') or '0') + inicio = (CfdiNomina + .select(fn.Max(CfdiNomina.folio).alias('mf')) + .where(CfdiNomina.serie==serie) + .order_by(SQL('mf')) + .scalar()) + + if inicio is None: + new = 1 + else: + new += 1 + + if folio > new: + new = folio + + return new + + def _validate_nomina(self, row): sn = {'si': True, 'no': False} data = row.copy() - return data + rfc = data.pop('rfc') + try: + data['empleado'] = Empleados.get(Empleados.rfc==rfc) + except Empleados.DoesNotExist: + msg = 'No existe el Empleado con RFC: {}'.format(rfc) + return {}, msg + + tipo_nomina = SATTipoNomina.get_by_key(row['tipo_nomina']) + if tipo_nomina is None: + msg = 'RFC: {}, Tipo de Nómina no existe: {}'.format(row['tipo_nomina']) + return {}, msg + + data['serie'] = self._get_serie(self) + data['folio'] = self._get_folio(self, data['serie']) + data['forma_pago'] = DEFAULT_SAT_NOMINA['FORMA_PAGO'] + data['uso_cfdi'] = DEFAULT_SAT_NOMINA['USO_CFDI'] + data['tipo_nomina'] = tipo_nomina + data['fecha_pago'] = util.calc_to_date(row['fecha_pago']) + data['fecha_inicial_pago'] = util.calc_to_date(row['fecha_inicial_pago']) + data['fecha_final_pago'] = util.calc_to_date(row['fecha_final_pago']) + data['dias_pagados'] = util.get_days(data['fecha_inicial_pago'], data['fecha_final_pago']) + + return data, '' + + def _validate_percepciones(self, headers, row): + total_gravado = 0.0 + total_exento = 0.0 + total_jubilacion = 0.0 + total_separacion = 0.0 + + data = [] + for i, key in enumerate(headers[::2]): + gravado = round(row[i * 2], DECIMALES) + exento = round(row[i * 2 + 1], DECIMALES) + if not gravado and not exento: + continue + tp = SATTipoPercepcion.get_by_key(key) + if tp is None: + continue + + total_gravado += gravado + total_exento += exento + if key in ('039', '044'): + total_jubilacion += gravado + exento + elif key in ('022', '023', '025'): + total_separacion += gravado + exento + new = { + 'tipo_percepcion': tp, + 'importe_gravado': gravado, + 'importe_exento': exento, + } + data.append(new) + + total_sueldos = round(total_gravado + total_exento, DECIMALES) + totals = { + 'total_gravado': total_gravado, + 'total_exento': total_exento, + 'total_jubilacion': total_jubilacion, + 'total_separacion': total_separacion, + 'total_sueldos': total_sueldos, + 'total_percepciones': round( + total_sueldos + total_jubilacion + total_separacion, DECIMALES) + } + + return data, totals, '' + + def _validate_deducciones(self, headers, row): + total_retenciones = 0.0 + total_otras_deducciones = 0.0 + + data = [] + for i, value in enumerate(row): + key = headers[0][i] + importe = round(value, DECIMALES) + if not importe: + continue + + td = SATTipoDeduccion.get_by_key(key) + if td is None: + continue + + if key == '002': + total_retenciones += importe + else: + total_otras_deducciones += importe + + new = { + 'tipo_deduccion': td, + 'importe': importe, + } + data.append(new) + + totals = { + 'total_retenciones': total_retenciones, + 'total_otras_deducciones': total_otras_deducciones, + 'total_deducciones': round( + total_retenciones + total_otras_deducciones, DECIMALES) + } + + return data, totals, '' + + def _validate_otros_pagos(self, headers, row): + total_otros_pagos = 0.0 + + data = [] + subsidio_causado = round(row[0], DECIMALES) + for i, value in enumerate(row): + if not i: + continue + + key = headers[0][i] + importe = round(value, DECIMALES) + if not importe: + continue + + td = SATTipoOtroPago.get_by_key(key) + if td is None: + continue + + total_otros_pagos += importe + + new = { + 'tipo_otro_pago': td, + 'importe': importe, + } + if key == '002': + new['subsidio_causado'] = subsidio_causado + data.append(new) + + totals = {'total_otros_pagos': total_otros_pagos} + + return data, totals, '' + + def _validate_horas_extras(self, row): + data = [] + for i, key in enumerate(row[::4]): + days = int(row[i * 4]) + key = row[i * 4 + 1] + the = SATTipoHoras.get_by_key(key) + if the is None: + continue + + hours = int(row[i * 4 + 2]) + importe = round(row[i * 4 + 3], DECIMALES) + if not hours or not importe: + continue + + new = { + 'dias': days, + 'tipos_horas': the, + 'horas_extra': hours, + 'importe_pagado': importe, + } + data.append(new) + + return data, '' + + def _validate_incapacidades(self, row): + data = [] + for i, key in enumerate(row[::3]): + key = row[i * 3] + ti = SATTipoIncapacidad.get_by_key(key) + if ti is None: + continue + + days = int(row[i * 4 + 1]) + importe = round(row[i * 4 + 2], DECIMALES) + if not days or not importe: + continue + + new = { + 'dias': ti, + 'tipo': days, + 'importe': importe, + } + data.append(new) + + return data, '' def _import(self): + util.log_file('nomina', kill=True) emisor = Emisor.select()[0] - rows, msg = util.import_nomina(emisor.rfc) - if not rows: + data, msg = util.import_nomina(emisor.rfc) + if not data: return {'ok': False, 'msg': msg} - for row in rows: - data = self._validate_import(self, row) - # ~ w = (Nomina.empleado.rfc==row['rfc']) + hp = data['percepciones'][0] + percepciones = data['percepciones'][2:] + hd = data['deducciones'][:1] + deducciones = data['deducciones'][2:] + ho = data['otros_pagos'][:1] + otros_pagos = data['otros_pagos'][2:] + horas_extras = data['horas_extras'][2:] + incapacidades = data['incapacidades'][2:] + + for i, row in enumerate(data['nomina']): + row['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal + row['regimen_fiscal'] = emisor.regimenes[0].key + row['registro_patronal'] = emisor.registro_patronal + new_nomina, msg = self._validate_nomina(self, row) + if msg: + util.log_file('nomina', msg) + continue + + new_percepciones, total_percepciones, msg = \ + self._validate_percepciones(self, hp, percepciones[i]) + if msg: + util.log_file('nomina', msg) + continue + + new_deducciones, total_deducciones, msg = \ + self._validate_deducciones(self, hd, deducciones[i]) + if msg: + util.log_file('nomina', msg) + continue + + new_otros_pagos, total_otros_pagos, msg = \ + self._validate_otros_pagos(self, ho, otros_pagos[i]) + if msg: + util.log_file('nomina', msg) + continue + + new_horas_extras, msg = self._validate_horas_extras(self, horas_extras[i]) + if msg: + util.log_file('nomina', msg) + continue + + new_incapacidades, msg = self._validate_incapacidades(self, incapacidades[i]) + if msg: + util.log_file('nomina', msg) + continue + + totals = total_percepciones.copy() + totals.update(total_deducciones) + totals.update(total_otros_pagos) + totals['subtotal'] = round(totals['total_percepciones'] + + totals['total_otros_pagos'], DECIMALES) + totals['descuento'] = totals['total_deducciones'] + totals['total'] = round(totals['subtotal'] - + totals['descuento'], DECIMALES) + with database_proxy.transaction(): - pass - # ~ if Empleados.select().where(w).exists(): - # ~ q = Empleados.update(**data).where(w) - # ~ q.execute() - # ~ else: - # ~ obj = Empleados.create(**data) + obj = CfdiNomina.create(**new_nomina) + for row in new_percepciones: + row['cfdi'] = obj + CfdiNominaPercepciones.create(**row) + for row in new_deducciones: + row['cfdi'] = obj + CfdiNominaDeducciones.create(**row) + for row in new_otros_pagos: + row['cfdi'] = obj + CfdiNominaOtroPago.create(**row) + for row in new_horas_extras: + row['cfdi'] = obj + CfdiNominaHorasExtra.create(**row) + for row in new_incapacidades: + row['cfdi'] = obj + CfdiNominaIncapacidad.create(**row) + + concepto = { + 'cfdi': obj, + 'valor_unitario': totals['subtotal'], + 'importe': totals['subtotal'], + 'descuento': totals['total_deducciones'], + } + CfdiNominaDetalle.create(**concepto) + + totals['cfdi'] = obj + CfdiNominaTotales.create(**totals) + + msg = 'Nómina importada correctamente' return {'ok': True, 'msg': msg} def _get(self): @@ -5653,6 +5932,7 @@ class CfdiNominaSeparacion(BaseModel): class CfdiNominaPercepciones(BaseModel): cfdi = ForeignKeyField(CfdiNomina) tipo_percepcion = ForeignKeyField(SATTipoPercepcion) + concepto = TextField(default='') importe_gravado = DecimalField(default=0.0, max_digits=20, decimal_places=6, auto_round=True) importe_exento = DecimalField(default=0.0, max_digits=20, decimal_places=6, @@ -5666,6 +5946,7 @@ class CfdiNominaPercepciones(BaseModel): class CfdiNominaDeducciones(BaseModel): cfdi = ForeignKeyField(CfdiNomina) tipo_deduccion = ForeignKeyField(SATTipoDeduccion) + concepto = TextField(default='') importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, auto_round=True) @@ -5673,6 +5954,7 @@ class CfdiNominaDeducciones(BaseModel): class CfdiNominaOtroPago(BaseModel): cfdi = ForeignKeyField(CfdiNomina) tipo_otro_pago = ForeignKeyField(SATTipoOtroPago) + concepto = TextField(default='') importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, auto_round=True) subsidio_causado = DecimalField(default=0.0, max_digits=20, decimal_places=6, diff --git a/source/app/settings.py b/source/app/settings.py index 4f2dcb4..487caae 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -137,6 +137,9 @@ USAR_TOKEN = False CANCEL_SIGNATURE = False PUBLIC = 'Público en general' DEFAULT_SAT_NOMINA = { + 'SERIE': 'N', + 'FORMA_PAGO': '99', + 'USO_CFDI': 'P01', 'CLAVE': '84111505', 'UNIDAD': 'ACT', 'DESCRIPCION': 'Pago de nómina', From 1cfea6978b9021ca1eec759e0ca65c7949863c34 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 28 Jan 2018 21:35:10 -0600 Subject: [PATCH 12/24] =?UTF-8?q?Timbrar=20y=20cancelar=20n=C3=B3mina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/controllers/cfdi_xml.py | 118 +++++--- source/app/controllers/main.py | 2 +- source/app/controllers/pac.py | 8 +- source/app/models/db.py | 10 + source/app/models/main.py | 416 ++++++++++++++++++++++++-- source/static/js/controller/nomina.js | 213 ++++++++++++- source/static/js/ui/nomina.js | 7 +- 7 files changed, 700 insertions(+), 74 deletions(-) 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')}, ] From 069136d4c577b208ed67b59d781c2d407b94db64 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 29 Jan 2018 20:42:13 -0600 Subject: [PATCH 13/24] Validar celdas con formato texto --- source/app/controllers/util.py | 2 +- source/app/models/main.py | 135 ++++++++++++++++++-------- source/static/js/controller/nomina.js | 2 +- 3 files changed, 98 insertions(+), 41 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index eb871c5..6852863 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -2960,6 +2960,6 @@ def log_file(name, msg='', kill=False): return with open(path, 'a') as fh: - line = '{} : {}'.format(str(now()), msg) + line = '{} : {}\n'.format(str(now()), msg) fh.write(line) return diff --git a/source/app/models/main.py b/source/app/models/main.py index 646308f..f87501d 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -5469,8 +5469,11 @@ class Empleados(BaseModel): @classmethod def remove(cls, id): - q = Empleados.delete().where(Empleados.id==id) - return bool(q.execute()) + try: + q = Empleados.delete().where(Empleados.id==id) + return bool(q.execute()) + except IntegrityError: + return False class CfdiNomina(BaseModel): @@ -5591,8 +5594,13 @@ class CfdiNomina(BaseModel): data = [] for i, key in enumerate(headers[::2]): - gravado = round(row[i * 2], DECIMALES) - exento = round(row[i * 2 + 1], DECIMALES) + gravado = 0.0 + exento = 0.0 + if isinstance(row[i * 2], float): + gravado = round(row[i * 2], DECIMALES) + if isinstance(row[i * 2 + 1], float): + exento = round(row[i * 2 + 1], DECIMALES) + if not gravado and not exento: continue tp = SATTipoPercepcion.get_by_key(key) @@ -5632,7 +5640,10 @@ class CfdiNomina(BaseModel): data = [] for i, value in enumerate(row): key = headers[0][i] - importe = round(value, DECIMALES) + importe = 0.0 + if isinstance(value, float): + importe = round(value, DECIMALES) + if not importe: continue @@ -5670,7 +5681,10 @@ class CfdiNomina(BaseModel): continue key = headers[0][i] - importe = round(value, DECIMALES) + importe = 0.0 + if isinstance(value, float): + importe = round(value, DECIMALES) + if not importe: continue @@ -5695,14 +5709,20 @@ class CfdiNomina(BaseModel): def _validate_horas_extras(self, row): data = [] for i, key in enumerate(row[::4]): - days = int(row[i * 4]) + days = 0 + if isinstance(row[i * 4], float): + days = int(row[i * 4]) key = row[i * 4 + 1] the = SATTipoHoras.get_by_key(key) if the is None: continue - hours = int(row[i * 4 + 2]) - importe = round(row[i * 4 + 3], DECIMALES) + hours = 0 + if isinstance(row[i * 4 + 2], float): + hours = int(row[i * 4 + 2]) + importe = 0.0 + if isinstance(row[i * 4 + 3], float): + importe = round(row[i * 4 + 3], DECIMALES) if not hours or not importe: continue @@ -5724,8 +5744,12 @@ class CfdiNomina(BaseModel): if ti is None: continue - days = int(row[i * 4 + 1]) - importe = round(row[i * 4 + 2], DECIMALES) + days = 0 + if isinstance(row[i * 3 + 1], float): + days = int(row[i * 3 + 1]) + importe = 0.0 + if isinstance(row[i * 3 + 2], float): + importe = round(row[i * 3 + 2], DECIMALES) if not days or not importe: continue @@ -5738,6 +5762,18 @@ class CfdiNomina(BaseModel): return data, '' + def _validate_exists(self, values): + result = (CfdiNomina + .select() + .where( + (CfdiNomina.empleado==values['empleado']) & + (CfdiNomina.fecha_pago==values['fecha_pago']) & + (CfdiNomina.fecha_inicial_pago==values['fecha_inicial_pago']) & + (CfdiNomina.fecha_final_pago==values['fecha_final_pago']) & + (CfdiNomina.total==values['total'])) + .exists()) + return result + def _import(self): util.log_file('nomina', kill=True) emisor = Emisor.select()[0] @@ -5805,34 +5841,46 @@ class CfdiNomina(BaseModel): 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: - row['cfdi'] = obj - CfdiNominaPercepciones.create(**row) - for row in new_deducciones: - row['cfdi'] = obj - CfdiNominaDeducciones.create(**row) - for row in new_otros_pagos: - row['cfdi'] = obj - CfdiNominaOtroPago.create(**row) - for row in new_horas_extras: - row['cfdi'] = obj - CfdiNominaHorasExtra.create(**row) - for row in new_incapacidades: - row['cfdi'] = obj - CfdiNominaIncapacidad.create(**row) + if self._validate_exists(self, new_nomina): + info = '{}'.format(new_nomina['empleado'].nombre_completo) + msg = 'Nomina existente: {}'.format(info) + util.log_file('nomina', msg) + continue - concepto = { - 'cfdi': obj, - 'valor_unitario': totals['subtotal'], - 'importe': totals['subtotal'], - 'descuento': totals['total_deducciones'], - } - CfdiNominaDetalle.create(**concepto) + try: + with database_proxy.transaction(): + obj = CfdiNomina.create(**new_nomina) + for row in new_percepciones: + row['cfdi'] = obj + CfdiNominaPercepciones.create(**row) + for row in new_deducciones: + row['cfdi'] = obj + CfdiNominaDeducciones.create(**row) + for row in new_otros_pagos: + row['cfdi'] = obj + CfdiNominaOtroPago.create(**row) + for row in new_horas_extras: + row['cfdi'] = obj + CfdiNominaHorasExtra.create(**row) + for row in new_incapacidades: + row['cfdi'] = obj + CfdiNominaIncapacidad.create(**row) - totals['cfdi'] = obj - CfdiNominaTotales.create(**totals) + concepto = { + 'cfdi': obj, + 'valor_unitario': totals['subtotal'], + 'importe': totals['subtotal'], + 'descuento': totals['total_deducciones'], + } + CfdiNominaDetalle.create(**concepto) + + totals['cfdi'] = obj + CfdiNominaTotales.create(**totals) + except Exception as e: + msg = 'ERROR: {}-{}'.format(new_nomina['serie'], new_nomina['folio']) + util.log_file('nomina', msg) + util.log_file('nomina', str(e)) + continue msg = 'Nómina importada correctamente' return {'ok': True, 'msg': msg} @@ -5854,6 +5902,7 @@ class CfdiNomina(BaseModel): .where(where) .join(Empleados) .switch(CfdiNomina) + .order_by(CfdiNomina.id) .dicts() ) return {'ok': True, 'rows': tuple(rows)} @@ -6018,8 +6067,11 @@ class CfdiNomina(BaseModel): deducciones = { 'TotalOtrasDeducciones': FORMAT.format(totals.total_otras_deducciones), - 'TotalImpuestosRetenidos': FORMAT.format(totals.total_retenciones), } + if totals.total_retenciones: + deducciones['TotalImpuestosRetenidos'] = \ + FORMAT.format(totals.total_retenciones) + rows = CfdiNominaDeducciones.select().where(CfdiNominaDeducciones.cfdi==cfdi) details = [] for row in rows: @@ -6109,8 +6161,9 @@ class CfdiNomina(BaseModel): return result['ok'], obj.error def _stamp(self): + msg = '' where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False)) - rows = CfdiNomina.select().where(where) + rows = CfdiNomina.select().where(where).order_by(CfdiNomina.id) util.log_file('nomina', kill=True) msg_error = '' ok_stamp = 0 @@ -6121,7 +6174,10 @@ class CfdiNomina(BaseModel): if result: msg = 'Recibo: {}-{}, timbrado correctamente'.format(row.serie, row.folio) ok_stamp += 1 + util.log_file('nomina', msg) else: + msg = 'Error la timbrar: {}-{}, {}'.format(row.serie, row.folio, msg) + util.log_file('nomina', msg) msg_error = msg break @@ -6129,6 +6185,7 @@ class CfdiNomina(BaseModel): if ok_stamp: msg = 'Se timbraron {} recibos'.format(ok_stamp) ok = True + error = False if msg_error: error = True diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 2416f75..753ff10 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -255,7 +255,7 @@ function delete_empleado(id){ $$('grid_employees').remove(id); msg_ok(msg) } else { - msg = 'No se pudo eliminar.' + msg = 'El Empleado tiene recibos timbrados' msg_error(msg) } }) From 5466726898ec17749b15fe981cf73af63cf17315 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 29 Jan 2018 21:57:44 -0600 Subject: [PATCH 14/24] =?UTF-8?q?Obtener=20registro=20de=20n=C3=B3mina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/controllers/util.py | 9 +++++++++ source/app/models/main.py | 7 +++++-- source/static/js/controller/nomina.js | 6 ++++++ source/static/js/ui/nomina.js | 2 ++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 6852863..f8fd5dc 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -2963,3 +2963,12 @@ def log_file(name, msg='', kill=False): line = '{} : {}\n'.format(str(now()), msg) fh.write(line) return + + +def get_log(name): + data = '' + name = '{}.log'.format(name) + path = _join(PATH_MEDIA, 'tmp', name) + if is_file(path): + data = open(path).read() + return data, name diff --git a/source/app/models/main.py b/source/app/models/main.py index f87501d..96784ca 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -171,6 +171,7 @@ def get_doc(type_doc, id, rfc): 'xml': 'application/xml', 'ods': 'application/octet-stream', 'zip': 'application/octet-stream', + 'nomlog': 'application/txt', } content_type = types.get(type_doc, 'application/pdf') if type_doc == 'xml': @@ -187,6 +188,8 @@ def get_doc(type_doc, id, rfc): data, file_name = Tickets.get_pdf(id) elif type_doc == 'xmlnom': data, file_name = CfdiNomina.get_xml(id) + elif type_doc == 'nomlog': + data, file_name = util.get_log('nomina') return data, file_name, content_type @@ -6018,9 +6021,9 @@ class CfdiNomina(BaseModel): nomina_receptor['Puesto'] = empleado.puesto.nombre if empleado.clabe: - nomina_receptor['CuentaBancaria'] = empleado.clabe + nomina_receptor['CuentaBancaria'] = empleado.clabe.replace('-', '') elif empleado.cuenta_bancaria: - nomina_receptor['CuentaBancaria'] = empleado.cuenta_bancaria + nomina_receptor['CuentaBancaria'] = empleado.cuenta_bancaria.replace('-', '') nomina_receptor['Banco'] = empleado.banco.key if empleado.salario_base: diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 753ff10..5f4e6aa 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -12,6 +12,7 @@ var nomina_controllers = { $$('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_log').attachEvent('onItemClick', cmd_nomina_log_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) @@ -468,4 +469,9 @@ function cancel_nomina(id){ } } }) +} + + +function cmd_nomina_log_click(){ + location = '/doc/nomlog/0' } \ No newline at end of file diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js index 9dca50f..69dcc65 100644 --- a/source/static/js/ui/nomina.js +++ b/source/static/js/ui/nomina.js @@ -18,6 +18,8 @@ var toolbar_nomina_util = [ type: 'iconButton', autowidth: true, icon: 'ticket'}, {view: 'button', id: 'cmd_nomina_sat', label: 'SAT', type: 'iconButton', autowidth: true, icon: 'check-circle'}, + {view: 'button', id: 'cmd_nomina_log', label: 'Log', + type: 'iconButton', autowidth: true, icon: 'download'}, {}, {view: 'button', id: 'cmd_nomina_cancel', label: 'Cancelar', type: 'iconButton', autowidth: true, icon: 'ban'}, From e33e3379d7a682e2ad5ce1469e0742e5402cba2f Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 29 Jan 2018 23:34:06 -0600 Subject: [PATCH 15/24] =?UTF-8?q?Importar=20catalogos=20del=20SAT=20para?= =?UTF-8?q?=20n=C3=B3mina?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/models/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/app/models/main.py b/source/app/models/main.py index 070bf98..1628847 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -6838,6 +6838,8 @@ def _migrate_tables(): migrate(*migrations) log.info('Tablas migradas correctamente...') + _importar_valores('', rfc) + return From e67a1756a825e0e63b8e76894263880efe5d2ead Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 29 Jan 2018 23:51:08 -0600 Subject: [PATCH 16/24] Fix - al generar xml --- source/app/controllers/cfdi_xml.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index 724ced1..475007f 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -164,7 +164,7 @@ class CFDI(object): if self._edu: name = 'xmlns:{}'.format(SAT['edu']['prefix']) attributes[name] = SAT['edu']['xmlns'] - schema_edu = SAT['edu']['schema'] + schema_edu = SAT['edu']['schema'] schema_nomina = '' if self._nomina: @@ -173,7 +173,8 @@ class CFDI(object): schema_nomina = SAT['nomina']['schema'] attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \ - schema_locales + schema_donativo + schema_ine + + schema_edu + schema_nomina + schema_locales + schema_donativo + schema_ine + schema_edu + \ + schema_nomina attributes.update(datos) if not 'Version' in attributes: From 06c32e56a882ac33b59a7a26bc617e7a3f11c133 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 30 Jan 2018 00:41:43 -0600 Subject: [PATCH 17/24] UI para importar XML --- source/app/controllers/util.py | 19 ++++++++ source/static/js/controller/admin.js | 65 ++++++++++++++++++++++++++++ source/static/js/ui/admin.js | 12 +++++ 3 files changed, 96 insertions(+) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 45474d6..02e9979 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1696,6 +1696,14 @@ def upload_file(rfc, opt, file_obj): name = '{}_nomina.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) + elif opt == 'cfdixml': + tmp = file_obj.filename.split('.') + ext = tmp[-1].lower() + if ext != 'xml': + msg = 'Extensión de archivo incorrecta, selecciona un archivo XML' + return {'status': 'server', 'name': msg, 'ok': False} + + return import_xml(file_obj.file.read()) if save_file(path, file_obj.file.read()): return {'status': 'server', 'name': file_obj.filename, 'ok': True} @@ -2979,3 +2987,14 @@ def get_log(name): if is_file(path): data = open(path).read() return data, name + + +def import_xml(stream): + try: + xml = ET.fromstring(stream.decode()) + except ET.ParseError: + return {'ok': False, 'status': 'error'} + + print (xml) + return {'ok': True, 'status': 'server'} + diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index ca1bc11..e8877af 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -78,7 +78,9 @@ var controllers = { $$('chk_usar_nomina').attachEvent('onItemClick', chk_config_item_click) $$('cmd_subir_bdfl').attachEvent('onItemClick', cmd_subir_bdfl_click) + $$('cmd_subir_cfdixml').attachEvent('onItemClick', cmd_subir_cfdixml_click) $$('up_bdfl').attachEvent('onUploadComplete', up_bdfl_upload_complete) + $$('up_cfdixml').attachEvent('onUploadComplete', up_cfdixml_upload_complete) } } @@ -1593,6 +1595,69 @@ function up_bdfl_upload_complete(response){ } +function cmd_subir_cfdixml_click(){ + var form = $$('form_upload_cfdixml') + + if (!form.validate()){ + msg = 'Valores inválidos' + msg_error(msg) + return + } + + var values = form.getValues() + + if($$('lst_cfdixml').count() < 1){ + msg = 'Selecciona un archivo XML' + msg_error(msg) + return + } + + if($$('lst_cfdixml').count() > 1){ + msg = 'Selecciona solo un archivo' + msg_error(msg) + return + } + + var cfdixml = $$('up_cfdixml').files.getItem($$('up_cfdixml').files.getFirstId()) + + var ext = [] + if(cfdixml.type.toLowerCase() != 'xml'){ + msg = 'Archivo inválido, se requiere un archivo XML' + msg_error(msg) + return + } + + msg = '¿Estás seguro de subir este archivo?' + webix.confirm({ + title: 'Importar CFDI', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('up_cfdixml').send() + } + } + }) +} + + +function up_cfdixml_upload_complete(response){ + if(response.status != 'server'){ + msg = 'Ocurrio un error al subir los archivos' + msg_error(msg) + return + } + + msg = 'Archivo importado correctamente' + msg_ok(msg) + + $$('form_upload_cfdixml').setValues({}) + $$('up_cfdixml').files.data.clearAll() +} + + function cmd_usuario_agregar_click(){ var form = $$('form_usuario') diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 906df6a..eb84171 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -675,6 +675,18 @@ var utilidades_archivos = [ {cols: [{}, {view: 'button', id: 'cmd_subir_bdfl', label: 'Subir base de datos de Factura Libre'}, {}]}, ]}, + {maxHeight: 15}, + {template: 'Importar archivo CFDI (XML)', type: 'section'}, + {view: 'form', id: 'form_upload_cfdixml', rows: [ + {cols: [{}, + {view: 'uploader', id: 'up_cfdixml', autosend: false, link: 'lst_cfdixml', + value: 'Seleccionar archivo XML', upload: '/files/cfdixml'}, {}]}, + {cols: [{}, + {view: 'list', id: 'lst_cfdixml', name: 'cfdixml', + type: 'uploader', autoheight: true, borderless: true}, {}]}, + {cols: [{}, {view: 'button', id: 'cmd_subir_cfdixml', + label: 'Importar CFDI'}, {}]}, + ]}, {}] From 5c0541799912e2caf901f091913ffb6fd985a56f Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 30 Jan 2018 09:59:28 -0600 Subject: [PATCH 18/24] Fix - Al eliminar nivel educativo --- source/app/models/main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/source/app/models/main.py b/source/app/models/main.py index 1628847..466a35a 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1474,6 +1474,13 @@ class NivelesEducativos(BaseModel): @classmethod def remove(cls, id): + obj = NivelesEducativos.get(NivelesEducativos.id==int(id)) + q = Grupos.delete().where(Grupos.nivel==obj) + try: + q.execute() + except IntegrityError: + return False + q = NivelesEducativos.delete().where(NivelesEducativos.id==int(id)) return bool(q.execute()) From 363f8bfce3748bbe09e44c78412210dea27dad02 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 30 Jan 2018 13:21:45 -0600 Subject: [PATCH 19/24] Cambiar version en xslt iedu --- source/xslt/iedu.xslt | 48 +++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/source/xslt/iedu.xslt b/source/xslt/iedu.xslt index 63ee02f..eb285cb 100644 --- a/source/xslt/iedu.xslt +++ b/source/xslt/iedu.xslt @@ -1,26 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + From b56907e74edc43d46cb1e557e56e1e8772ad4c0e Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 30 Jan 2018 13:59:27 -0600 Subject: [PATCH 20/24] =?UTF-8?q?Cambiar=20restricci=C3=B3n=20en=20tabla?= =?UTF-8?q?=20Puestos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/models/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index 466a35a..df35435 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -5502,11 +5502,14 @@ class Departamentos(BaseModel): class Puestos(BaseModel): departamento = ForeignKeyField(Departamentos, null=True) - nombre = TextField(default='', unique=True) + nombre = TextField(default='') descripcion = TextField(default='') class Meta: order_by = ('nombre',) + indexes = ( + (('departamento', 'nombre'), True), + ) @classmethod def get_by_depto(cls, puesto, depto): From 2b7a51db970e7b0dbe989729bddcf0b9e3ada6a7 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 30 Jan 2018 18:37:41 -0600 Subject: [PATCH 21/24] Mensaje cuando no hay documentos por timbrar --- source/app/models/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/app/models/main.py b/source/app/models/main.py index df35435..b17b060 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -6329,6 +6329,11 @@ class CfdiNomina(BaseModel): where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False)) rows = CfdiNomina.select().where(where).order_by(CfdiNomina.id) util.log_file('nomina', kill=True) + + if not len(rows): + msg = 'Sin recibos por timbrar' + return {'ok': True, 'msg_ok': msg} + msg_error = '' ok_stamp = 0 for row in rows: From 4bd5a710080f8955ae490ac28141b76fa5587b18 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 30 Jan 2018 18:55:00 -0600 Subject: [PATCH 22/24] Fix - Issue #158 --- source/app/models/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index b17b060..81acedc 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -4369,7 +4369,6 @@ class PreFacturas(BaseModel): def _calculate_totals(self, invoice, products): tax_locales = Configuracion.get_bool('chk_config_tax_locales') tax_decimals = Configuracion.get_bool('chk_config_tax_decimals') - tax_decimals = True subtotal = 0 descuento_cfdi = 0 totals_tax = {} From f9f80a52e7df08e8aca13eab29fa90e956523d18 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 30 Jan 2018 20:27:50 -0600 Subject: [PATCH 23/24] =?UTF-8?q?N=C3=B3mina=20-=20Obtener=20serie=20y=20f?= =?UTF-8?q?olio=20de=20configuraci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/models/main.py | 12 +++- source/static/js/controller/admin.js | 84 ++++++++++++++++++++++++++++ source/static/js/ui/admin.js | 4 ++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index 81acedc..6753d3d 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -340,8 +340,14 @@ class Configuracion(BaseModel): .where(Configuracion.clave.in_(fields)) ) values = {r.clave: util.get_bool(r.valor) for r in data} + fields = ( + 'txt_ticket_printer', + 'txt_config_nomina_serie', + 'txt_config_nomina_folio', + ) tp = 'txt_ticket_printer' - values[tp] = Configuracion.get_(tp) + for f in fields: + values[f] = Configuracion.get_(f) return values if keys['fields'] == 'correo': @@ -5699,13 +5705,13 @@ class CfdiNomina(BaseModel): order_by = ('fecha',) def _get_serie(self): - serie = Configuracion.get_('chk_config_serie_nomina') + serie = Configuracion.get_('txt_config_nomina_serie') if not serie: serie = DEFAULT_SAT_NOMINA['SERIE'] return serie def _get_folio(self, serie): - folio = int(Configuracion.get_('chk_config_folio_nomina') or '0') + folio = int(Configuracion.get_('txt_config_nomina_folio') or '0') inicio = (CfdiNomina .select(fn.Max(CfdiNomina.folio).alias('mf')) .where(CfdiNomina.serie==serie) diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index e8877af..6fe960a 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -75,6 +75,8 @@ var controllers = { $$('chk_ticket_edit_cant').attachEvent('onItemClick', chk_config_item_click) $$('chk_ticket_total_up').attachEvent('onItemClick', chk_config_item_click) $$('txt_ticket_printer').attachEvent('onKeyPress', txt_ticket_printer_key_press) + $$('txt_config_nomina_serie').attachEvent('onKeyPress', txt_config_nomina_serie_press) + $$('txt_config_nomina_folio').attachEvent('onKeyPress', txt_config_nomina_folio_press) $$('chk_usar_nomina').attachEvent('onItemClick', chk_config_item_click) $$('cmd_subir_bdfl').attachEvent('onItemClick', cmd_subir_bdfl_click) @@ -1908,6 +1910,88 @@ function txt_ticket_printer_key_press(code, e){ } +function txt_config_nomina_serie_press(code, e){ + var value = this.getValue() + if(code != 13){ + return + } + + if(!value.trim()){ + webix.ajax().del('/config', {id: 'txt_config_nomina_serie'}, function(text, xml, xhr){ + var msg = 'Serie de Nómina borrado correctamente' + if(xhr.status == 200){ + msg_ok(msg) + }else{ + msg = 'No se pudo eliminar' + msg_error(msg) + } + }) + return + } + + webix.ajax().post('/config', {'txt_config_nomina_serie': value.toUpperCase()}, { + 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 = 'Serie de Nómina guardada correctamente' + msg_ok(msg) + }else{ + msg_error(values.msg) + } + } + }) + +} + + +function txt_config_nomina_folio_press(code, e){ + var value = this.getValue() + if(code != 13){ + return + } + + if(!value.trim()){ + webix.ajax().del('/config', {id: 'txt_config_nomina_folio'}, function(text, xml, xhr){ + var msg = 'Folio de Nómina borrado correctamente' + if(xhr.status == 200){ + msg_ok(msg) + }else{ + msg = 'No se pudo eliminar' + msg_error(msg) + } + }) + return + } + + if(!value.trim().is_number()){ + msg = 'El Folio de Nómina debe ser un número' + msg_error(msg) + return + } + + webix.ajax().post('/config', {'txt_config_nomina_folio': value}, { + 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 = 'Folio de Nómina guardado correctamente' + msg_ok(msg) + }else{ + msg_error(values.msg) + } + } + }) + +} + + function cmd_niveles_educativos_click(){ admin_ui_niveles_educativos.init() $$('win_niveles_educativos').show() diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index eb84171..36ba071 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -645,6 +645,10 @@ var options_admin_otros = [ {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'}, {}]}, {}] From f28ab95b50bde56de018ca4ebeb79987ffa0bb52 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 30 Jan 2018 21:43:09 -0600 Subject: [PATCH 24/24] =?UTF-8?q?N=C3=B3mina=20-=20Borrar=20en=20lote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/models/main.py | 43 ++++++++++++++------------- source/static/js/controller/nomina.js | 30 ++++++++++++------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index 6753d3d..ae915c7 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -6398,27 +6398,30 @@ class CfdiNomina(BaseModel): return cls._stamp(cls) @classmethod - def remove(cls, id): - obj = CfdiNomina.get(CfdiNomina.id==id) - if obj.uuid: - return False + def remove(cls, ids): + ids = util.loads(ids) + for id in ids: + obj = CfdiNomina.get(CfdiNomina.id==id) + if obj.uuid: + continue + with database_proxy.transaction(): + 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() + obj.delete_instance() - 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()) + return True @classmethod def get_xml(cls, id): diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js index 5f4e6aa..efc5c19 100644 --- a/source/static/js/controller/nomina.js +++ b/source/static/js/controller/nomina.js @@ -296,17 +296,25 @@ function cmd_nomina_without_stamp_click(){ function cmd_nomina_delete_click(){ - var row = $$('grid_nomina').getSelectedItem() + var rows = $$('grid_nomina').getSelectedItem() - if (row == undefined){ - msg = 'Selecciona un registro' + if (rows == undefined){ + msg = 'Selecciona al menos un registro' msg_error(msg) return } + var ids = [] + if(Array.isArray(rows)){ + for(var i in rows){ + ids.push(rows[i].id) + } + }else{ + ids.push(rows.id) + } - msg = '¿Estás seguro de eliminar el registro?

' - msg += row['empleado'] + ' (' + row['fecha_pago'] + ')' - msg += '

ESTA ACCIÓN NO SE PUEDE DESHACER

' + msg = '¿Estás seguro de eliminar los recibos seleccionado?

' + msg += 'ESTA ACCIÓN NO SE PUEDE DESHACER

' + msg += 'Solo se eliminan recibos no timbrados' webix.confirm({ title: 'Eliminar Nomina', ok: 'Si', @@ -315,18 +323,18 @@ function cmd_nomina_delete_click(){ text: msg, callback:function(result){ if (result){ - delete_nomina(row['id']) + delete_nomina(ids) } } }) } -function delete_nomina(id){ - webix.ajax().del('/nomina', {id: id}, function(text, xml, xhr){ - var msg = 'Registro eliminado correctamente' +function delete_nomina(ids){ + webix.ajax().del('/nomina', {id: ids}, function(text, xml, xhr){ + var msg = 'Registros eliminados correctamente' if (xhr.status == 200){ - $$('grid_nomina').remove(id); + get_nomina() msg_ok(msg) } else { msg = 'No se pudo eliminar.'