diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py index b402199..0cd1c36 100644 --- a/source/app/controllers/helper.py +++ b/source/app/controllers/helper.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -#~ import falcon +import os import re import smtplib import ssl @@ -13,8 +13,8 @@ from email.mime.text import MIMEText from email import encoders from email.utils import formatdate -import os import requests +from escpos import printer from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Image from reportlab.lib import colors @@ -949,3 +949,92 @@ class SeaFileAPI(object): resp = requests.get(url, headers=self._headers) return resp.json() + +class PrintTicket(object): + LINE = '------------------------------------------------\n' + TITLES = 'CANT. U ARTICULO P.U. TOTAL\n' + LEYENDA = 'GRACIAS POR SU COMPRA\n\nGuarde este ticket para cualquier ' \ + 'aclaración.\nComprobante simplificado de operación con\npúblico en ' \ + 'general de acuerdo al Art. 37\nFracc II inc. v del Reglamento del\n' \ + 'Código Fiscal de la Federación.\n\n' + + def __init__(self, info): + self.p = self._init_printer(info) + + def _init_printer(self, info): + try: + if info['ip']: + p = printer.Network(info['ip']) + else: + p = printer.Usb(*info['usb']) + p.codepage = 'cp850' + return p + except Exception as e: + print (e) + return + + def _set(self, *data): + self.p.set(*data) + return + + def _t(self, text): + self.p.text(text) + return + + def _l(self): + self._t(self.LINE) + return + + def printer(self, data): + if self.p is None: + return False + + self._emisor(data['emisor']) + self._receptor(data['receptor']) + self._ticket(data['ticket']) + self._products(data['products']) + self._footer(data['ticket']) + self.p.cut() + return True + + def _emisor(self, data): + self._set('center', 'B', 'B', 2, 2) + self._t(data['name']) + self._set('center', 'B', 'B', 2) + self._t(data['rfc']) + self._set('center', 'A') + self._t(data['address']) + return + + def _receptor(self, data): + self._set('center', 'B', 'B', 2) + self._t(data['name']) + return + + def _ticket(self, data): + self._set('left', 'B', 'B', 2) + self._t(data['title']) + self._set('left', 'A') + self._t(data['date']) + return + + def _products(self, data): + self._l() + self._t(self.TITLES) + self._l() + for p in data: + self._t(p) + self._l() + self._t('Total artículos: {}\n'.format(len(data))) + self._l() + return + + def _footer(self, data): + self._set('right', 'B', 'B', 2) + self._t(data['total']) + self._set('center', 'A') + self._t(data['letters']) + self._t(self.LEYENDA) + self._set('center', 'A', 'B') + self._t('empresalibre.net') + return \ No newline at end of file diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 6e17cec..56dcd6f 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -36,7 +36,7 @@ import pyqrcode from dateutil import parser from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice, \ - SeaFileAPI + SeaFileAPI, PrintTicket 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, DIR_FACTURAS @@ -1307,6 +1307,15 @@ def upload_file(rfc, opt, file_obj): name = '{}_3.3.json'.format(rfc.lower()) path = _join(PATH_MEDIA, 'templates', name) + elif opt == 'txt_plantilla_ticket': + 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 = '{}_ticket.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'templates', name) elif opt == 'txt_plantilla_donataria': tmp = file_obj.filename.split('.') ext = tmp[-1].lower() @@ -1705,12 +1714,12 @@ class ImportFacturaLibreGambas(object): ('cfdifacturas', 'Facturas'), ('categorias', 'Categorias'), ('productos', 'Productos'), - # ~ ('tickets', 'Tickets'), + ('tickets', 'Tickets'), ) for source, target in tables: data[target] = self._get_table(source) - # ~ data['Socios'] += self._clientes + data['Socios'] += self._clientes return data @@ -2505,3 +2514,8 @@ class ImportFacturaLibre(object): data.append(new) return data + + +def print_ticket(data, info): + p = PrintTicket(info) + return p.printer(data) diff --git a/source/app/models/db.py b/source/app/models/db.py index 7ec35a0..d88373c 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -268,6 +268,8 @@ class StorageEngine(object): return main.Tickets.cancel(values) if opt == 'invoice': return main.Tickets.invoice(values) + if opt == 'print': + return main.Tickets.printer(values) def get_tickets(self, values): return main.Tickets.get_by(values) diff --git a/source/app/models/main.py b/source/app/models/main.py index a9bc1c5..e4faaff 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -17,7 +17,7 @@ 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 + CANCEL_SIGNATURE, PUBLIC FORMAT = '{0:.2f}' @@ -211,6 +211,24 @@ class Configuracion(BaseModel): values['default_unidad'] = SATUnidades.get_default() return values + if keys['fields'] == 'configtemplates': + try: + emisor = Emisor.select()[0] + is_ong = emisor.es_ong + except IndexError: + is_ong = False + + values = {'txt_plantilla_donataria': is_ong} + fields = ( + ('chk_usar_punto_de_venta', 'txt_plantilla_ticket'), + ) + + for s, key in fields: + value = util.get_bool(Configuracion.get_(s)) + values[key] = value + + return values + if keys['fields'] == 'correo': fields = ('correo_servidor', 'correo_puerto', 'correo_ssl', 'correo_usuario', 'correo_contra', 'correo_copia', @@ -231,6 +249,7 @@ class Configuracion(BaseModel): 'txt_plantilla_factura_32', 'txt_plantilla_factura_33', 'txt_plantilla_factura_33j', + 'txt_plantilla_ticket', 'txt_plantilla_donataria', ) data = (Configuracion @@ -4242,6 +4261,14 @@ class Tickets(BaseModel): filters = (fy & fm) return filters + if opt == 'dates': + dates = util.loads(values['range']) + filters = Tickets.fecha.between( + util.get_date(dates['start']), + util.get_date(dates['end'], True) + ) + return filters + return @classmethod @@ -4328,6 +4355,63 @@ class Tickets(BaseModel): doc = util.to_pdf(data, data['emisor']['rfc']) return doc, name + def _format_ticket(self, id): + emisor = util.get_dict(Emisor.select().dicts()[0]) + ticket = Tickets.select().where(Tickets.id==id).dicts()[0] + products = TicketsDetalle.get_by_print(id) + + emisor['name'] = '{}\n'.format(emisor['nombre']) + emisor['rfc'] = 'RFC: {}\n'.format(emisor['rfc']) + interior = '' + if emisor['no_interior']: + interior = ', {}'.format(emisor['no_interior']) + colonia = '' + if emisor['colonia']: + colonia = ', Col. {}'.format(emisor['colonia']) + municipio = '' + if emisor['municipio']: + municipio = ', {}'.format(emisor['municipio']) + estado = '' + if emisor['estado']: + estado = ', {}'.format(emisor['estado']) + cp = '' + if emisor['codigo_postal']: + cp = ', C.P. {}'.format(emisor['codigo_postal']) + pais = '' + if emisor['pais']: + pais = ', {}'.format(emisor['pais']) + direccion = '{} {}{}{}{}{}{}{}\n\n'.format(emisor['calle'], + emisor['no_exterior'], interior, colonia, municipio, estado, cp, + pais) + emisor['address'] = direccion + + ticket['title'] = 'Ticket: {}{}\n\n'.format(ticket['serie'], + ticket['folio']) + ticket['date'] = 'Fecha y hora: {}\n'.format(ticket['fecha']) + ticket['letters'] = '{}\n\n'.format( + util.to_letters(ticket['total'], 'peso')) + ticket['total'] = 'TOTAL: $ {:>12,.2f}\n\n'.format(ticket['total']) + + data = { + 'emisor': emisor, + 'receptor': {'name': '{}\n\n'.format(PUBLIC)}, + 'ticket': ticket, + 'products': products, + } + return data + + @classmethod + def printer(cls, values): + id = int(values['id']) + info = {'ip': '', 'usb': (int('1ba0', 16), int('2204', 16))} + data = cls._format_ticket(cls, id) + result = util.print_ticket(data, info) + msg = 'Ticket impreso correctamente' + if not result: + msg = 'Asegurate de que la impresora este conectada y funcionando.' + result = {'ok': result, 'msg': msg} + return result + class TicketsDetalle(BaseModel): ticket = ForeignKeyField(Tickets) @@ -4356,6 +4440,21 @@ class TicketsDetalle(BaseModel): return precio_final + @classmethod + def get_by_print(cls, id): + products = TicketsDetalle.select().where(TicketsDetalle.ticket==id) + lines = [] + for p in products: + price_with_tax = cls._with_tax(cls, p) + importe = round(price_with_tax * float(p.cantidad), DECIMALES) + l = '{:>6,.2f} {:<4} {:<14} {:>9,.2f} {:>10,.2f}\n'.format( + p.cantidad, p.producto.unidad.name, p.descripcion, + price_with_tax, importe + ) + lines.append(l) + + return lines + @classmethod def get_by_ticket(cls, id): data = [] diff --git a/source/app/settings.py b/source/app/settings.py index 1c96b5f..c1298c4 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -117,4 +117,5 @@ IMPUESTOS = { DEFAULT_SAT_PRODUCTO = '01010101' DIR_FACTURAS = 'facturas' USAR_TOKEN = False -CANCEL_SIGNATURE = False \ No newline at end of file +CANCEL_SIGNATURE = False +PUBLIC = 'Público en general' \ No newline at end of file diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 2cfe14c..b87ca2c 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -49,6 +49,7 @@ var controllers = { $$('txt_plantilla_factura_32').attachEvent('onItemClick', txt_plantilla_factura_32_click) $$('txt_plantilla_factura_33').attachEvent('onItemClick', txt_plantilla_factura_33_click) $$('txt_plantilla_factura_33j').attachEvent('onItemClick', txt_plantilla_factura_33j_click) + $$('txt_plantilla_ticket').attachEvent('onItemClick', txt_plantilla_ticket_click) $$('txt_plantilla_donataria').attachEvent('onItemClick', txt_plantilla_donataria_click) $$('chk_config_ocultar_metodo_pago').attachEvent('onItemClick', chk_config_item_click) $$('chk_config_ocultar_condiciones_pago').attachEvent('onItemClick', chk_config_item_click) @@ -330,13 +331,29 @@ function get_admin_usuarios(){ } +function set_config_templates(){ + webix.ajax().get('/config', {'fields': 'configtemplates'}, { + error: function(text, data, xhr) { + msg = 'Error al consultar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json() + Object.keys(values).forEach(function(key){ + show(key, values[key]) + }) + } + }) +} + + function get_config_values(opt){ if(opt == undefined){ return } if(opt == 'templates'){ - show('txt_plantilla_donataria', $$('chk_ong').getValue()) + set_config_templates() } webix.ajax().get('/config', {'fields': opt}, { @@ -822,6 +839,44 @@ function txt_plantilla_factura_33_click(e){ } +function txt_plantilla_ticket_click(e){ + + var body_elements = [ + {cols: [{width: 100}, {view: 'uploader', id: 'up_template', autosend: true, link: 'lst_files', + value: 'Seleccionar archivo', upload: '/files/txt_plantilla_ticket', + width: 200}, {width: 100}]}, + {view: 'list', id: 'lst_files', type: 'uploader', autoheight:true, + borderless: true}, + {}, + {cols: [{}, {view: 'button', label: 'Cerrar', autowidth: true, + click:("$$('win_template').close();")}, {}]} + ] + + var w = webix.ui({ + view: 'window', + id: 'win_template', + modal: true, + position: 'center', + head: 'Subir Plantilla Ticket ODT', + body: { + view: 'form', + elements: body_elements, + } + }) + + w.show() + + $$('up_template').attachEvent('onUploadComplete', function(response){ + if(response.ok){ + $$('txt_plantilla_ticket').setValue(response.name) + msg_ok('Plantilla cargada correctamente') + }else{ + msg_error(response.name) + } + }) +} + + function txt_plantilla_factura_32_click(e){ var body_elements = [ diff --git a/source/static/js/controller/tickets.js b/source/static/js/controller/tickets.js index f920c39..59033a8 100644 --- a/source/static/js/controller/tickets.js +++ b/source/static/js/controller/tickets.js @@ -7,6 +7,7 @@ var tickets_controllers = { init: function(){ $$('cmd_nuevo_ticket').attachEvent('onItemClick', cmd_nuevo_ticket_click) $$('cmd_ticket_to_invoice').attachEvent('onItemClick', cmd_ticket_to_invoice_click) + $$('cmd_ticket_report').attachEvent('onItemClick', cmd_ticket_report_click) $$('cmd_generar_ticket').attachEvent('onItemClick', cmd_generar_ticket_click) $$('cmd_cerrar_ticket').attachEvent('onItemClick', cmd_cerrar_ticket_click) $$('cmd_new_invoice_from_ticket').attachEvent('onItemClick', cmd_new_invoice_from_ticket_click) @@ -20,8 +21,10 @@ var tickets_controllers = { $$('grid_tdetails').attachEvent('onItemClick', grid_ticket_details_click) $$('grid_tdetails').attachEvent('onBeforeEditStop', grid_tickets_details_before_edit_stop) $$('gt_productos_found').attachEvent('onValueSuggest', gt_productos_found_click) + $$('cmd_ticket_filter_today').attachEvent('onItemClick', cmd_ticket_filter_today_click) $$('filter_year_ticket').attachEvent('onChange', filter_year_ticket_change) $$('filter_month_ticket').attachEvent('onChange', filter_month_ticket_change) + $$('filter_dates_ticket').attachEvent('onChange', filter_dates_ticket_change) $$('chk_is_invoice_day').attachEvent('onChange', chk_is_invoice_day_change) $$('grid_tickets_active').attachEvent('onItemDblClick', grid_tickets_active_double_click) $$('grid_tickets_invoice').attachEvent('onItemDblClick', grid_tickets_invoice_double_click) @@ -78,20 +81,33 @@ function get_tickets(filters){ } +function cmd_ticket_filter_today_click(){ + get_tickets() +} + + function filter_year_ticket_change(nv, ov){ var fm = $$('filter_month_ticket') - filters = {'opt': 'yearmonth','year': nv, 'month': fm.getValue()} + filters = {'opt': 'yearmonth', 'year': nv, 'month': fm.getValue()} get_tickets(filters) } function filter_month_ticket_change(nv, ov){ var fy = $$('filter_year_ticket') - filters = {'opt': 'yearmonth','year': fy.getValue(), 'month': nv} + filters = {'opt': 'yearmonth', 'year': fy.getValue(), 'month': nv} get_tickets(filters) } +function filter_dates_ticket_change(range){ + if(range.start != null && range.end != null){ + filters = {'opt': 'dates', 'range': range} + get_tickets(filters) + } +} + + function configuracion_inicial_ticket(){ current_dates_tickets() get_tickets() @@ -651,11 +667,54 @@ function ticket_notes_key_up(){ } +function print_ticket(id){ + var data = new Object() + data['opt'] = 'print' + data['id'] = id + + webix.ajax().post('tickets', 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){ + msg_ok(values.msg) + }else{ + msg_error(values.msg) + } + } + }) +} + + function grid_tickets_click(id, e, node){ - //~ var row = this.getItem(id) if(id.column == 'pdf'){ //~ window.open('/doc/tpdf/' + id, '_blank') - get_ticket_pdf(id) + get_ticket_pdf(id.row) + return } + if(id.column == 'print'){ + print_ticket(id.row) + return + } + +} + + +function cmd_ticket_report_click(){ + webix.toPDF($$('grid_tickets'), { + ignore: {'pdf': true, 'print': true}, + filename: 'Reporte_Tickets', + columns:{ + index: true, + serie: {width: 50}, + folio: {width: 50}, + fecha: {width: 125}, + estatus: true, + total: {css: 'right'}, + } + }) } \ No newline at end of file diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index b56e370..191e870 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -27,6 +27,7 @@ var form_editar_usuario_elementos = [ ]} ] + var admin_ui_windows = { init: function(){ webix.ui({ @@ -470,12 +471,12 @@ var options_templates = [ {maxHeight: 15}, {cols: [{maxWidth: 15}, {view: 'search', id: 'txt_plantilla_factura_32', name: 'plantilla_factura_32', - label: 'Plantilla Factura v3.2 (ODT): ', labelPosition: 'top', + label: 'Plantilla Factura v3.2 (ODS): ', labelPosition: 'top', icon: 'file'}, {}]}, {maxHeight: 20}, {cols: [{maxWidth: 15}, {view: 'search', id: 'txt_plantilla_factura_33', labelPosition: 'top', - label: 'Plantilla Factura v3.3 (ODT): ', icon: 'file'}, {}]}, + label: 'Plantilla Factura v3.3 (ODS): ', icon: 'file'}, {}]}, {maxHeight: 20}, {cols: [{maxWidth: 15}, {view: 'search', id: 'txt_plantilla_factura_33j', name: 'plantilla_factura_33j', @@ -483,9 +484,14 @@ var options_templates = [ icon: 'file'}, {}]}, {maxHeight: 20}, {cols: [{maxWidth: 15}, + {view: 'search', id: 'txt_plantilla_ticket', name: 'plantilla_ticket', + label: 'Plantilla para Tickets (ODS): ', labelPosition: 'top', + icon: 'file'}, {view: 'search', id: 'txt_plantilla_donataria', name: 'plantilla_donataria', label: 'Plantilla Donataria (solo ONGs): ', labelPosition: 'top', - icon: 'file'}, {}]}, + icon: 'file'}, + {}]}, + {maxHeight: 20}, {}] diff --git a/source/static/js/ui/tickets.js b/source/static/js/ui/tickets.js index f3a8b67..553898c 100644 --- a/source/static/js/ui/tickets.js +++ b/source/static/js/ui/tickets.js @@ -6,12 +6,17 @@ var toolbar_tickets = [ {view: 'button', id: 'cmd_ticket_to_invoice', label: 'Facturar', type: 'iconButton', autowidth: true, icon: 'file-code-o'}, {}, + {view: 'button', id: 'cmd_ticket_report', label: 'Reporte', + type: 'iconButton', autowidth: true, icon: 'bars'}, + {}, {view: 'button', id: 'cmd_cancelar_ticket', label: 'Cancelar', type: 'iconButton', autowidth: true, icon: 'ban'}, ] var toolbar_tickets_filter = [ + {view: 'button', id: 'cmd_ticket_filter_today', label: 'Hoy', type: 'iconButton', + autowidth: true, icon: 'filter'}, {view: 'richselect', id: 'filter_year_ticket', label: 'Año', labelAlign: 'right', labelWidth: 50, width: 150, options: []}, {view: 'richselect', id: 'filter_month_ticket', label: 'Mes', @@ -25,8 +30,8 @@ var grid_tickets_cols = [ {id: 'index', header: '#', adjust: 'data', css: 'right', footer: {content: 'countRows', colspan: 3, css: 'right'}}, {id: "id", header:"ID", hidden:true}, - {id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data", - sort:"string", hidden: true}, + {id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "header", + sort: "string"}, {id: 'folio', header: ['Folio', {content: 'numberFilter'}], adjust: 'header', sort: 'int', css: 'right', footer: {text: 'Tickets', colspan: 3}}, {id: "fecha", header: ["Fecha y Hora"], @@ -34,11 +39,12 @@ var grid_tickets_cols = [ {id: "estatus", header: ["Estatus", {content: "selectFilter"}], adjust: "data", sort:"string"}, {id: 'total', header: ['Total', {content: 'numberFilter'}], width: 150, - sort: 'int', format: webix.i18n.priceFormat, css: 'right'}, + sort: 'int', format: webix.i18n.priceFormat, css: 'right', + footer: {content: 'summColumn', css: 'right'}}, {id: "cliente", header: ["Razón Social", {content: "selectFilter"}], fillspace:true, sort:"string", hidden: true}, {id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')}, - {id: 'print', header: 'I', adjust: 'data', template: get_icon('print')}, + {id: 'print', header: '', adjust: 'data', template: get_icon('print')}, ]