diff --git a/source/app/conf.py.example b/source/app/conf.py.example index 0892815..968da07 100644 --- a/source/app/conf.py.example +++ b/source/app/conf.py.example @@ -10,7 +10,7 @@ DEFAULT_PASSWORD = 'blades3.3' #~ Establece una ruta accesible para el servidor web -LOG_PATH = '/srv/empresa/logs/empresalibre.log' +LOG_PATH = '/srv/empresa/logs/empresa-libre.log' # ~ Establece los valores para sincronizar los backups de la base de datos # ~ por ejemplo @@ -21,5 +21,4 @@ LOG_PATH = '/srv/empresa/logs/empresalibre.log' # ~ 'REPO': 'id_repo', # ~ } - SEAFILE_SERVER = {} \ No newline at end of file diff --git a/source/app/controllers/conf.py.example b/source/app/controllers/conf.py.example index d400ea7..5da41e8 100644 --- a/source/app/controllers/conf.py.example +++ b/source/app/controllers/conf.py.example @@ -1,7 +1,7 @@ #!/usr/bin/env python -DEBUG = True +DEBUG = False #~ Ecodex ID_INTEGRADOR = '' diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 2bb3f65..2440e98 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -19,6 +19,7 @@ import uuid import zipfile from io import BytesIO +from pathlib import Path from smtplib import SMTPException, SMTPAuthenticationError from xml.etree import ElementTree as ET @@ -26,6 +27,7 @@ try: import uno from com.sun.star.beans import PropertyValue from com.sun.star.awt import Size + from com.sun.star.view.PaperFormat import LETTER APP_LIBO = True except: APP_LIBO = False @@ -35,9 +37,9 @@ from dateutil import parser from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice, \ SeaFileAPI -from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \ +from settings import DEBUG, MV, log, template_lookup, COMPANIES, DB_SAT, \ PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE, \ - PATH_XMLSEC, TEMPLATE_CANCEL, DEFAULT_SAT_PRODUCTO, DECIMALES + PATH_XMLSEC, TEMPLATE_CANCEL, DEFAULT_SAT_PRODUCTO, DECIMALES, DIR_FACTURAS from settings import SEAFILE_SERVER @@ -843,6 +845,7 @@ class LIBO(object): if self._template is None: return b'' + self._template.setPrinter(self._set_properties({'PaperFormat': LETTER})) self._render(data) path = '{}.ods'.format(tempfile.mkstemp()[1]) @@ -1459,16 +1462,66 @@ def backup_dbs(): return +def _validar_directorios(path_bk, target): + path = Path(_join(path_bk, target)) + path.mkdir(parents=True, exist_ok=True) + return str(path) + + +def local_copy(files): + if not MV: + return + + path_bk = _join(str(Path.home()), DIR_FACTURAS) + if not os.path.isdir(path_bk): + msg = 'No existe la carpeta: facturas' + log.error(msg) + return + + args = 'df -P {} | tail -1 | cut -d" " -f 1'.format(path_bk) + try: + result = _call(args) + log.info(result) + except: + pass + # ~ if result != 'empresalibre\n': + # ~ log.info(result) + # ~ msg = 'Asegurate de que exista la carpeta para sincronizar' + # ~ log.error(msg) + # ~ return + # ~ except subprocess.CalledProcessError: + # ~ msg = 'No se pudo obtener la ruta para sincronizar' + # ~ log.error(msg) + # ~ return + + try: + for obj, name, target in files: + path = _validar_directorios(path_bk, target) + path_file = _join(path, name) + m = 'wb' + if name.endswith('xml'): + m = 'w' + save_file(path_file, obj, m) + except Exception as e: + log.error(e) + + return + + def sync_cfdi(auth, files): + local_copy(files) + if DEBUG: return + if not auth['REPO']: + return + seafile = SeaFileAPI(SEAFILE_SERVER['URL'], auth['USER'], auth['PASS']) if seafile.is_connect: for f in files: seafile.update_file( f, auth['REPO'], 'Facturas/{}/'.format(f[2]), auth['PASS']) - return @@ -1671,7 +1724,7 @@ class ImportFacturaLibre(object): data = [] for row in rows: - new = {t: row[s] for s, t in fields} + new = {t: row[s] for s, t in fields if row[s]} data.append(new) return data diff --git a/source/app/main.py b/source/app/main.py index e30da00..3063849 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -18,7 +18,6 @@ from controllers.main import (AppEmpresas, AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco, AppMovimientosBanco ) -from settings import DEBUG db = StorageEngine() @@ -51,13 +50,13 @@ api.add_route('/cuentasbanco', AppCuentasBanco(db)) api.add_route('/movbanco', AppMovimientosBanco(db)) -if DEBUG: - api.add_sink(static, '/static') +# ~ Activa si usas waitress +# ~ api.add_sink(static, '/static') session_options = { 'session.type': 'file', - 'session.cookie_expires': True, + 'session.cookie_expires': 3600, 'session.data_dir': '/tmp/cache/data', 'session.lock_dir': '/tmp/cache/lock', } diff --git a/source/app/main_debug.ini b/source/app/main_debug.ini index 065dcfc..043ea43 100644 --- a/source/app/main_debug.ini +++ b/source/app/main_debug.ini @@ -6,3 +6,5 @@ master = true processes = 4 threads = 4 py-autoreload = 1 +thunder-lock = true +static-map = /static=../static diff --git a/source/app/main_linux.ini b/source/app/main_linux.ini index 119d72c..e028cb8 100644 --- a/source/app/main_linux.ini +++ b/source/app/main_linux.ini @@ -1,8 +1,13 @@ [uwsgi] -http = 127.0.0.1:8000 +http = :8000 +uid = user +gid = user +chdir = /home/USER/.opt/empresa-libre/source/app wsgi-file = main.py callable = app master = true processes = 4 threads = 4 - +thunder-lock = true +static-map = /static=../static +logger = file:../../../empresa-libre-uwsgi.log diff --git a/source/app/models/main.py b/source/app/models/main.py index 976b390..3015961 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -226,6 +226,86 @@ class Tags(BaseModel): order_by = ('tag',) +class TipoDireccion(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoTitulo(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoTelefono(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoCorreo(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoPariente(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoResponsable(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoMovimientoAlumno(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + +class TipoMovimientoAlmacen(BaseModel): + nombre = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + def __str__(self): + return self.nombre + + class Usuarios(BaseModel): usuario = TextField(unique=True) nombre = TextField(default='') @@ -1074,6 +1154,45 @@ class SATBancos(BaseModel): return {'ok': result} +class SATNivelesEducativos(BaseModel): + name = TextField(index=True) + + class Meta: + order_by = ('name',) + + def __str__(self): + return self.name + + +class NivelesEducativos(BaseModel): + nombre = TextField() + autorizacion = TextField(default='') + + class Meta: + order_by = ('nombre',) + indexes = ( + (('nombre', 'autorizacion'), True), + ) + + def __str__(self): + return '{} ({})'.format(self.nombre, self.autorizacion) + + +class Grupos(BaseModel): + nivel = ForeignKeyField(NivelesEducativos) + grado = TextField(default='') + nombre = TextField(default='') + + class Meta: + order_by = ('nivel', 'grado', 'nombre') + indexes = ( + (('nivel', 'grado', 'nombre'), True), + ) + + def __str__(self): + return '{} {} {}'.format(self.nivel.nombre, self.grado, self.nombre) + + class CuentasBanco(BaseModel): de_emisor = BooleanField(default=False) activa = BooleanField(default=True) @@ -1671,6 +1790,132 @@ class Socios(BaseModel): return bool(q.execute()) +class Contactos(BaseModel): + socio = ForeignKeyField(Socios) + titulo = ForeignKeyField(TipoTitulo) + foto = TextField(default='') + nombre = TextField(index=True) + paterno = TextField(index=True) + materno = TextField(default='') + fecha_nacimiento = DateField(null=True) + notas = TextField(default='') + + class Meta: + order_by = ('socio', 'nombre') + indexes = ( + (('socio', 'nombre', 'paterno', 'materno'), True), + ) + + +class ContactoDirecciones(BaseModel): + contacto = ForeignKeyField(Contactos) + tipo = ForeignKeyField(TipoDireccion) + direccion = TextField() + + class Meta: + order_by = ('contacto',) + indexes = ( + (('contacto', 'tipo', 'direccion'), True), + ) + + +class ContactoTelefonos(BaseModel): + contacto = ForeignKeyField(Contactos) + tipo = ForeignKeyField(TipoTelefono) + telefono = TextField() + + class Meta: + order_by = ('contacto',) + indexes = ( + (('contacto', 'tipo', 'telefono'), True), + ) + + +class ContactoCorreos(BaseModel): + contacto = ForeignKeyField(Contactos) + tipo = ForeignKeyField(TipoCorreo) + correo = TextField() + + class Meta: + order_by = ('contacto',) + indexes = ( + (('contacto', 'tipo', 'correo'), True), + ) + + +class Alumnos(BaseModel): + rfc = TextField(null=True) + curp = TextField(index=True, unique=True) + foto = TextField(default='') + nombre = TextField(index=True) + paterno = TextField(index=True) + materno = TextField(default='') + calle = TextField(default='') + no_exterior = TextField(default='') + no_interior = TextField(default='') + colonia = TextField(default='') + municipio = TextField(default='') + estado = TextField(default='') + pais = TextField(default='') + codigo_postal = TextField(default='') + notas = TextField(default='') + telefonos = TextField(default='') + correos = TextField(default='') + es_activo = BooleanField(default=True) + fecha_alta = DateField(default=util.now) + fecha_nacimiento = DateField(null=True) + factura = ForeignKeyField(Socios, null=True) + grupo = ForeignKeyField(Grupos, null=True) + + def __str__(self): + t = '{} {} {}' + return t.format(self.nombre, self.paterno, self.materno) + + class Meta: + order_by = ('nombre', 'paterno') + + +class AlumnosParientes(BaseModel): + alumno = ForeignKeyField(Alumnos) + tipo_pariente = ForeignKeyField(TipoPariente) + foto = TextField(default='') + nombre = TextField(index=True) + paterno = TextField(index=True) + materno = TextField(default='') + fecha_nacimiento = DateField(null=True) + puede_recoger = BooleanField(default=False) + + class Meta: + order_by = ('alumno',) + + +class ParienteDirecciones(BaseModel): + pariente = ForeignKeyField(AlumnosParientes) + tipo = ForeignKeyField(TipoDireccion) + direccion = TextField() + + class Meta: + order_by = ('pariente',) + + +class ParienteTelefonos(BaseModel): + pariente = ForeignKeyField(AlumnosParientes) + tipo = ForeignKeyField(TipoTelefono) + telefono = TextField() + + class Meta: + order_by = ('pariente',) + + +class ParienteCorreos(BaseModel): + pariente = ForeignKeyField(AlumnosParientes) + tipo = ForeignKeyField(TipoCorreo) + correo = TextField() + + class Meta: + order_by = ('pariente',) + + class Almacenes(BaseModel): nombre = TextField(default='') ubicacion = TextField(default='') @@ -1882,6 +2127,21 @@ class Productos(BaseModel): return bool(q.execute()) +class RangosPrecios(BaseModel): + producto = ForeignKeyField(Productos) + descripcion = TextField(default='') + desde = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + hasta = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = IntegerField(default=0) + + class Meta: + order_by = ('producto',) + + class Facturas(BaseModel): cliente = ForeignKeyField(Socios) version = TextField(default=CURRENT_CFDI) @@ -1967,6 +2227,7 @@ class Facturas(BaseModel): def get_xml(cls, id): obj = Facturas.get(Facturas.id==id) name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc) + cls._sync_xml(cls, obj) return obj.xml, name #~ Revisar @@ -2056,8 +2317,12 @@ class Facturas(BaseModel): return values @classmethod - def get_pdf(cls, id, rfc): - emisor = Emisor.select()[0] + def get_pdf(cls, id, rfc, sync=True): + try: + emisor = Emisor.select()[0] + except IndexError: + return b'', 'sin_datos_de_emisor.pdf' + obj = Facturas.get(Facturas.id==id) name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc) if obj.uuid is None: @@ -2066,6 +2331,11 @@ class Facturas(BaseModel): values = cls._get_not_in_xml(cls, obj, emisor) data = util.get_data_from_xml(obj, values) doc = util.to_pdf(data, emisor.rfc) + + if sync: + target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/') + cls._sync_pdf(cls, doc, name, target) + return doc, name @classmethod @@ -2095,6 +2365,27 @@ class Facturas(BaseModel): def _sync(self, id, auth): return Facturas.sync(id, auth) + @util.run_in_thread + def _sync_pdf(self, pdf, name_pdf, target): + auth = Emisor.get_auth() + files = ( + (pdf, name_pdf, target), + ) + util.sync_cfdi(auth, files) + return + + @util.run_in_thread + def _sync_xml(self, obj): + emisor = Emisor.select()[0] + auth = Emisor.get_auth() + name_xml = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc) + target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/') + files = ( + (obj.xml, name_xml, target), + ) + util.sync_cfdi(auth, files) + return + @util.run_in_thread def _actualizar_saldo_cliente(self, invoice): if invoice.tipo_comprobante == 'T': @@ -2159,17 +2450,15 @@ class Facturas(BaseModel): @classmethod def sync(cls, id, auth): - if not auth['REPO']: - return - obj = Facturas.get(Facturas.id==id) if obj.uuid is None: msg = 'La factura no esta timbrada' return - pdf, name_pdf = cls.get_pdf(id, auth['RFC']) + emisor = Emisor.select()[0] + pdf, name_pdf = cls.get_pdf(id, auth['RFC'], False) name_xml = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc) - target = str(obj.fecha)[:7].replace('-', '/') + target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/') files = ( (obj.xml, name_xml, target), (pdf, name_pdf, target), @@ -2343,7 +2632,7 @@ class Facturas(BaseModel): totals_tax = {} total_trasladados = None total_retenciones = None - total_iva = 0 + # ~ total_iva = 0 locales_traslados = 0 locales_retenciones = 0 @@ -2376,52 +2665,66 @@ class Facturas(BaseModel): FacturasDetalle.create(**product) + base = product['importe'] - product['descuento'] for tax in p.impuestos: + impuesto_producto = round(float(tax.tasa) * base, DECIMALES) + if tax.tipo == 'T' and tax.key != '000': + total_trasladados = (total_trasladados or 0) + impuesto_producto + elif tax.tipo == 'R' and tax.key != '000': + total_retenciones = (total_retenciones or 0) + impuesto_producto + elif tax.tipo == 'T' and tax.key == '000': + locales_traslados += impuesto_producto + elif tax.tipo == 'R' and tax.key == '000': + locales_retenciones += impuesto_producto + if tax.id in totals_tax: - totals_tax[tax.id].importe += importe + totals_tax[tax.id].base += base + totals_tax[tax.id].suma_impuestos += impuesto_producto else: - tax.importe = importe + tax.base = base + tax.suma_impuestos = impuesto_producto totals_tax[tax.id] = tax for tax in totals_tax.values(): - if tax.tipo == 'E' or tax.tipo == 'R': + # ~ if tax.tipo == 'E' or tax.tipo == 'R': + if tax.tipo == 'E': continue - import_tax = round(float(tax.tasa) * tax.importe, DECIMALES) - if tax.key == '000': - locales_traslados += import_tax - else: - total_trasladados = (total_trasladados or 0) + import_tax - if tax.name == 'IVA': - total_iva += import_tax + # ~ import_tax = round(float(tax.tasa) * tax.importe, DECIMALES) + # ~ if tax.key == '000': + # ~ locales_traslados += import_tax + # ~ else: + # ~ total_trasladados = (total_trasladados or 0) + import_tax + # ~ if tax.name == 'IVA': + # ~ total_iva += import_tax invoice_tax = { 'factura': invoice.id, 'impuesto': tax.id, - 'base': tax.importe, - 'importe': import_tax, + 'base': tax.base, + 'importe': tax.suma_impuestos, } FacturasImpuestos.create(**invoice_tax) - for tax in totals_tax.values(): - if tax.tipo == 'E' or tax.tipo == 'T': - continue - if tax.tasa == round(Decimal(2/3), 6): - import_tax = round(float(tax.tasa) * total_iva, DECIMALES) - else: - import_tax = round(float(tax.tasa) * tax.importe, DECIMALES) - if tax.key == '000': - locales_retenciones += import_tax - else: - total_retenciones = (total_retenciones or 0) + import_tax + # ~ for tax in totals_tax.values(): + # ~ if tax.tipo == 'E' or tax.tipo == 'T': + # ~ continue + # ~ if tax.tasa == round(Decimal(2/3), 6): + # ~ import_tax = round(float(tax.tasa) * total_iva, DECIMALES) + # ~ else: + # ~ import_tax = round(float(tax.tasa) * tax.importe, DECIMALES) + # ~ if tax.key == '000': + # ~ locales_retenciones += import_tax + # ~ else: + # ~ total_retenciones = (total_retenciones or 0) + import_tax - invoice_tax = { - 'factura': invoice.id, - 'impuesto': tax.id, - 'base': tax.importe, - 'importe': import_tax, - } - FacturasImpuestos.create(**invoice_tax) + # ~ invoice_tax = { + # ~ 'factura': invoice.id, + # ~ 'impuesto': tax.id, + # ~ 'base': tax.base, + # ~ 'importe': tax.suma_impuestos, + # ~ } + # ~ FacturasImpuestos.create(**invoice_tax) total = subtotal - descuento_cfdi + \ (total_trasladados or 0) - (total_retenciones or 0) \ @@ -3169,24 +3472,6 @@ class FacturasComplementos(BaseModel): return {r.nombre: util.loads(r.valores) for r in query} -# ~ class CfdiPagosFacturas(BaseModel): - # ~ pago = ForeignKeyField(CfdiPagos) - # ~ factura = ForeignKeyField(Facturas) - # ~ numero = IntegerField(default=1) - # ~ saldo_anterior = DecimalField(default=0.0, max_digits=20, decimal_places=6, - # ~ auto_round=True) - # ~ importe = DecimalField(default=0.0, max_digits=18, decimal_places=6, - # ~ auto_round=True) - # ~ saldo = DecimalField(default=0.0, max_digits=18, decimal_places=6, - # ~ auto_round=True) - - # ~ class Meta: - # ~ order_by = ('pago',) - # ~ indexes = ( - # ~ (('pago', 'factura', 'numero'), True), - # ~ ) - - class PreFacturasRelacionadas(BaseModel): factura = ForeignKeyField(PreFacturas, related_name='original') factura_origen = ForeignKeyField(PreFacturas, related_name='relacion') @@ -3468,6 +3753,94 @@ class PreFacturasImpuestos(BaseModel): return data +class CamposPersonalizados(BaseModel): + nombre = TextField() + slug = TextField(unique=True) + + class Meta: + order_by = ('nombre',) + + +class FacturasPersonalizados(BaseModel): + factura = ForeignKeyField(Facturas) + campo = TextField() + valor = TextField() + + class Meta: + order_by = ('factura',) + + +class Tickets(BaseModel): + cliente = ForeignKeyField(Socios, null=True) + serie = TextField(default='') + folio = IntegerField(default=0) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) + forma_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) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_trasladados = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + estatus = TextField(default='Generado') + notas = TextField(default='') + factura = ForeignKeyField(Facturas, null=True) + cancelado = BooleanField(default=False) + vendedor = TextField(default='') + comision = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + cambio = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('fecha',) + + +class TicketsDetalle(BaseModel): + ticket = ForeignKeyField(Tickets) + producto = ForeignKeyField(Productos, null=True) + descripcion = TextField(default='') + cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + precio_final = 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) + + class Meta: + order_by = ('ticket',) + + +class TicketsImpuestos(BaseModel): + ticket = ForeignKeyField(Tickets) + impuesto = ForeignKeyField(SATImpuestos) + base = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('ticket',) + indexes = ( + (('ticket', 'impuesto'), True), + ) + + +class SeriesProductos(BaseModel): + factura = ForeignKeyField(FacturasDetalle, null=True) + ticket = ForeignKeyField(TicketsDetalle, null=True) + serie = TextField(default='') + + class Meta: + order_by = ('serie',) + + def authenticate(args): respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''} values = util.get_con(args['rfc']) @@ -3583,15 +3956,22 @@ def _init_values(rfc): def _crear_tablas(rfc): tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion, - Folios, Registro, + Folios, Registro, CamposPersonalizados, Emisor, Facturas, FacturasDetalle, FacturasImpuestos, FacturasPagos, - FacturasRelacionadas, FacturasComplementos, Almacenes, Productos, + FacturasRelacionadas, FacturasComplementos, FacturasPersonalizados, + SeriesProductos, Almacenes, Productos, RangosPrecios, PreFacturas, PreFacturasDetalle, PreFacturasImpuestos, - PreFacturasRelacionadas, + PreFacturasRelacionadas, Tickets, TicketsDetalle, TicketsImpuestos, SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes, SATTipoRelacion, SATUnidades, SATUsoCfdi, SATBancos, - Socios, Tags, Usuarios, CuentasBanco, TipoCambio, MovimientosBanco, - CfdiPagos, + SATNivelesEducativos, + Socios, Contactos, ContactoCorreos, ContactoDirecciones, + ContactoTelefonos, + Tags, Usuarios, CuentasBanco, TipoCambio, MovimientosBanco, + TipoCorreo, TipoDireccion, TipoPariente, TipoResponsable, TipoTelefono, + TipoTitulo, TipoMovimientoAlumno, TipoMovimientoAlmacen, + CfdiPagos, NivelesEducativos, Alumnos, AlumnosParientes, Grupos, + ParienteDirecciones, ParienteTelefonos, ParienteCorreos, Emisor.regimenes.get_through_model(), Socios.tags.get_through_model(), Productos.impuestos.get_through_model(), diff --git a/source/app/settings.py b/source/app/settings.py index db494d8..58a0807 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -21,7 +21,7 @@ except ImportError: DEBUG = DEBUG -VERSION = '0.2.1' +VERSION = '1.2.0' EMAIL_SUPPORT = ('soporte@empresalibre.net',) BASE_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -115,3 +115,4 @@ IMPUESTOS = { 'CEDULAR': '000', } DEFAULT_SAT_PRODUCTO = '01010101' +DIR_FACTURAS = 'facturas' \ No newline at end of file diff --git a/source/bin/libeay32.dll b/source/bin/libeay32.dll new file mode 100644 index 0000000..e94703c Binary files /dev/null and b/source/bin/libeay32.dll differ diff --git a/source/bin/openssl.exe b/source/bin/openssl.exe new file mode 100644 index 0000000..7ebb7c7 Binary files /dev/null and b/source/bin/openssl.exe differ diff --git a/source/bin/ssleay32.dll b/source/bin/ssleay32.dll new file mode 100644 index 0000000..5c90915 Binary files /dev/null and b/source/bin/ssleay32.dll differ diff --git a/source/db/valores_iniciales.json b/source/db/valores_iniciales.json index 27e6b54..fd4f604 100644 --- a/source/db/valores_iniciales.json +++ b/source/db/valores_iniciales.json @@ -27,6 +27,16 @@ {"key": "ACT", "name": "Actividad", "activo": false} ] }, +{ + "tabla": "SATNivelesEducativos", + "datos": [ + {"name": "Preescolar"}, + {"name": "Primaria"}, + {"name": "Secundaria"}, + {"name": "Profesional técnico"}, + {"name": "Bachillerato o su equivalente"} + ] +}, { "tabla": "SATTipoRelacion", "datos": [ diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index ee9bd95..2e03ff9 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -82,6 +82,22 @@ function get_regimen_fiscal(){ } +function validar_timbrar(){ + webix.ajax().sync().get('/values/validartimbrar', function(text, data){ + var values = data.json() + if(!values.ok){ + msg_error(values.msg) + $$('cmd_timbrar').disable() + }else{ + if(values.msg){ + msg_error(values.msg) + } + $$('cmd_timbrar').enable() + } + }) +} + + function default_config(){ webix.ajax().sync().get('/values/taxes', function(text, data){ var values = data.json() @@ -96,18 +112,7 @@ function default_config(){ table_pt.clear() table_totals.clear() - webix.ajax().sync().get('/values/validartimbrar', function(text, data){ - var values = data.json() - if(!values.ok){ - msg_error(values.msg) - $$('cmd_timbrar').disable() - }else{ - if(values.msg){ - msg_error(values.msg) - } - $$('cmd_timbrar').enable() - } - }) + validar_timbrar() webix.ajax().sync().get('/values/configtimbrar', function(text, data){ var values = data.json() @@ -702,6 +707,69 @@ function calculate_taxes(){ } +function calcular_impuestos(){ + var tmp = null + var subtotal = 0 + var id = 2 + var grid_totals = $$('grid_totals') + var impuesto_producto = 0 + var impuesto = null + + table_totals.clear() + grid_totals.clearAll() + grid_totals.add({id: 1, concepto: 'SubTotal', importe: 0}) + + grid.eachRow(function(row){ + var product = grid.getItem(row) + var importe = parseFloat(product.importe) + subtotal += importe + query = table_pt.chain().find({'product': product.id}).data() + for(var tax of query){ + impuesto = table_taxes.findOne({'id': tax.tax}) + if(impuesto.tipo == 'E'){ + continue + } + + var base = importe + if(impuesto.tipo == 'R'){ + base = (importe * -1).round(DECIMALES) + } + impuesto_producto = (impuesto.tasa * base).round(DECIMALES) + + tmp = table_totals.findOne({'tax': tax.tax}) + if(tmp === null){ + table_totals.insert({'tax': tax.tax, 'importe': impuesto_producto}) + }else{ + tmp.importe += impuesto_producto + table_totals.update(tmp) + } + + } + }) + + var tipo = '' + var concepto = '' + + query = table_totals.chain().data() + for(var t of query){ + tax = table_taxes.findOne({'id': t.tax}) + if(tax.tipo == 'E'){ + continue + } + tipo = 'Traslado ' + if(tax.tipo == 'R'){ + tipo = 'Retención ' + } + concepto = tipo + tax.name + ' (' + tax.tasa + ')' + grid_totals.add({id: id, concepto: concepto, importe: t.importe}) + id += 1 + } + + var row = {importe: subtotal} + grid_totals.updateItem(1, row) +} + + function set_product(values){ var taxes = values.taxes var values = values.row @@ -729,7 +797,8 @@ function set_product(values){ table_pt.insert(v) } } - calculate_taxes() + //~ calculate_taxes() + calcular_impuestos() } @@ -846,7 +915,8 @@ function grid_details_before_edit_stop(state, editor){ row['importe'] = (cantidad * precio_final).round(DECIMALES) grid.refresh() - calculate_taxes() + //~ calculate_taxes() + calcular_impuestos() } @@ -855,7 +925,8 @@ function grid_details_click(id, e, node){ return } grid.remove(id.row) - calculate_taxes() + //~ calculate_taxes() + calcular_impuestos() } @@ -874,7 +945,8 @@ function grid_details_header_click(id){ callback:function(result){ if (result){ grid.clearAll() - calculate_taxes() + //~ calculate_taxes() + calcular_impuestos() } } }) @@ -1299,7 +1371,8 @@ function refacturar_preinvoice(id){ for(var p of values.rows){ agregar_preproducto(p) } - calculate_taxes() + //~ calculate_taxes() + calcular_impuestos() $$('tv_invoice').getTabbar().setValue('Generar') } }) diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index 3887849..1dfb30f 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -200,6 +200,7 @@ function multi_change(prevID, nextID){ if(active == 'invoices_home'){ current_dates() get_invoices() + validar_timbrar() } gi = $$('grid_invoices') return diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index edda18f..532fe7f 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -10,7 +10,7 @@ var grid_cfdi_cliente_cols = [ css: 'right'}, {id: 'uuid', header: ['UUID', {content: 'textFilter'}], width: 250, sort: 'string'}, - {id: "fecha", header: ["Fecha y Hora"], width: 150, sort: 'date'}, + {id: "fecha", header: ["Fecha y Hora"], width: 150, sort: 'string'}, {id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}], adjust: 'header', sort: 'string'}, {id: "estatus", header: ["Estatus", {content: "selectFilter"}], @@ -202,7 +202,7 @@ var grid_invoices_cols = [ {id: "uuid", header: ["UUID", {content: "textFilter"}], adjust: "data", sort:"string", hidden:true}, {id: "fecha", header: ["Fecha y Hora"], - adjust: "data", sort: "date"}, + adjust: "data", sort: "string"}, {id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}], adjust: 'header', sort: 'string'}, {id: "estatus", header: ["Estatus", {content: "selectFilter"}],