From b777e1774fdbf426470dcf650a500ef2959cadbd Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 25 Dec 2017 23:30:34 -0600 Subject: [PATCH] Generar factura de ticket --- source/app/models/db.py | 2 + source/app/models/main.py | 190 ++++++++++++++++++---- source/static/js/controller/partners.js | 2 +- source/static/js/controller/tickets.js | 200 +++++++++++++++++++++++- source/static/js/ui/tickets.js | 53 ++++--- 5 files changed, 392 insertions(+), 55 deletions(-) diff --git a/source/app/models/db.py b/source/app/models/db.py index 89df7f6..7a48236 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -263,6 +263,8 @@ class StorageEngine(object): return main.Tickets.add(values) if opt == 'cancel': return main.Tickets.cancel(values) + if opt == 'invoice': + return main.Tickets.invoice(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 803427f..46a1683 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1790,7 +1790,7 @@ class Socios(BaseModel): fields = cls._clean(cls, values) try: obj = Socios.create(**fields) - except IntegrityError: + except IntegrityError as e: msg = 'Ya existe el RFC y Razón Social' data = {'ok': False, 'row': {}, 'new': True, 'msg': msg} return data @@ -2789,17 +2789,9 @@ class Facturas(BaseModel): totals_tax[tax.id] = tax for tax in totals_tax.values(): - # ~ 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 invoice_tax = { 'factura': invoice.id, @@ -2809,26 +2801,6 @@ class Facturas(BaseModel): } 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 - - # ~ 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) \ + locales_traslados - locales_retenciones @@ -4006,6 +3978,162 @@ class Tickets(BaseModel): data = {'ok': True, 'row': row} return data + def _get_folio_invoice(self, serie): + inicio = (Facturas + .select(fn.Max(Facturas.folio).alias('mf')) + .where(Facturas.serie==serie) + .order_by(SQL('mf')) + .scalar()) + + if inicio is None: + inicio = 1 + else: + inicio += 1 + + return inicio + + def _cancel_tickets(self, invoice, tickets): + query = (Tickets + .update(estatus='Facturado', cancelado=True, factura=invoice) + .where(Tickets.id.in_(tickets)) + ) + result = query.execute() + print (result) + return + + def _calculate_totals_invoice(self, invoice, tickets): + subtotal = 0 + descuento_cfdi = 0 + totals_tax = {} + total_trasladados = None + total_retenciones = None + + details = TicketsDetalle.select().where(TicketsDetalle.ticket.in_(tickets)) + + for detail in details: + product = {} + p = detail.producto + product['unidad'] = p.unidad.key + product['clave'] = p.clave + product['clave_sat'] = p.clave_sat + + product['factura'] = invoice.id + product['producto'] = p.id + product['descripcion'] = detail.descripcion + + cantidad = float(detail.cantidad) + valor_unitario = float(detail.valor_unitario) + descuento = float(detail.descuento) + precio_final = valor_unitario - descuento + importe = round(cantidad * precio_final, DECIMALES) + + product['cantidad'] = cantidad + product['valor_unitario'] = valor_unitario + product['descuento'] = round(descuento * cantidad, DECIMALES) + product['precio_final'] = precio_final + product['importe'] = round(cantidad * valor_unitario, DECIMALES) + + descuento_cfdi += product['descuento'] + subtotal += product['importe'] + + 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].base += base + totals_tax[tax.id].suma_impuestos += impuesto_producto + else: + tax.base = base + tax.suma_impuestos = impuesto_producto + totals_tax[tax.id] = tax + + for tax in totals_tax.values(): + if tax.tipo == 'E': + continue + + + 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) + total_mn = round(total * invoice.tipo_cambio, DECIMALES) + data = { + 'subtotal': subtotal, + 'descuento': descuento_cfdi, + 'total': total, + 'total_mn': total_mn, + 'total_trasladados': total_trasladados, + 'total_retenciones': total_retenciones, + } + return data + + @classmethod + def invoice(cls, values): + is_invoice_day = util.get_bool(values['is_invoice_day']) + id_client = int(values['client']) + tickets = util.loads(values['tickets']) + + if is_invoice_day: + filters = ( + Socios.rfc == 'XAXX010101000' and + Socios.slug == 'publico_en_general') + try: + client = Socios.get(filters) + except Socios.DoesNotExist: + msg = 'No existe el cliente Público en General. Agregalo primero.' + data = {'ok': False, 'msg': msg} + return data + else: + client = Socios.get(Socios.id==id_client) + + emisor = Emisor.select()[0] + data = {} + data['cliente'] = client + data['serie'] = 'T' + data['folio'] = cls._get_folio_invoice(cls, data['serie']) + data['forma_pago'] = client.forma_pago.key + data['tipo_cambio'] = 1.00 + data['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal + if client.uso_cfdi is None: + data['uso_cfdi'] = 'P01' + else: + data['uso_cfdi'] = client.uso_cfdi.key + data['regimen_fiscal'] = emisor.regimenes[0].key + + with database_proxy.atomic() as txn: + obj = Facturas.create(**data) + totals = cls._calculate_totals_invoice(cls, obj, tickets) + obj.subtotal = totals['subtotal'] + obj.descuento = totals['descuento'] + obj.total_trasladados = totals['total_trasladados'] + obj.total_retenciones = totals['total_retenciones'] + obj.total = totals['total'] + obj.saldo = totals['total'] + obj.total_mn = totals['total_mn'] + obj.save() + cls._cancel_tickets(cls, obj, tickets) + + msg = 'Factura generada correctamente.

Enviando a timbrar' + data = {'ok': True, 'msg': msg, 'id': obj.id} + return data + @classmethod def cancel(cls, values): id = int(values['id']) @@ -4021,6 +4149,10 @@ class Tickets(BaseModel): if not opt: return + if opt == 'active': + filters = (Tickets.cancelado==False) + return filters + if opt == 'today': t = util.today() filters = ( diff --git a/source/static/js/controller/partners.js b/source/static/js/controller/partners.js index 93b4346..74185e4 100644 --- a/source/static/js/controller/partners.js +++ b/source/static/js/controller/partners.js @@ -155,7 +155,7 @@ function cmd_save_partner_click(id, e, node){ if (values.ok) { update_grid_partner(values) } else { - msg_error(msg) + msg_error(values.msg) } } }) diff --git a/source/static/js/controller/tickets.js b/source/static/js/controller/tickets.js index 74060ca..741b5f1 100644 --- a/source/static/js/controller/tickets.js +++ b/source/static/js/controller/tickets.js @@ -11,6 +11,8 @@ var tickets_controllers = { $$('cmd_new_invoice_from_ticket').attachEvent('onItemClick', cmd_new_invoice_from_ticket_click) $$('cmd_close_ticket_invoice').attachEvent('onItemClick', cmd_cerrar_ticket_click) $$('cmd_cancelar_ticket').attachEvent('onItemClick', cmd_cancelar_ticket_click) + $$('cmd_move_tickets_right').attachEvent('onItemClick', cmd_move_tickets_right_click) + $$('cmd_move_tickets_left').attachEvent('onItemClick', cmd_move_tickets_left_click) $$('tsearch_product_key').attachEvent('onKeyPress', tsearch_product_key_press) $$('grid_tdetails').attachEvent('onItemClick', grid_ticket_details_click) $$('grid_tdetails').attachEvent('onBeforeEditStop', grid_tickets_details_before_edit_stop) @@ -18,8 +20,13 @@ var tickets_controllers = { $$('filter_year_ticket').attachEvent('onChange', filter_year_ticket_change) $$('filter_month_ticket').attachEvent('onChange', filter_month_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) + $$('tsearch_client_key').attachEvent('onKeyPress', tsearch_client_key_press) + $$('grid_ticket_clients_found').attachEvent('onValueSuggest', grid_ticket_clients_found_click) webix.extend($$('grid_tickets'), webix.ProgressBar) + webix.extend($$('grid_tickets_active'), webix.ProgressBar) } } @@ -87,8 +94,38 @@ function configuracion_inicial_ticket(){ } +function get_active_tickets(grid){ + filters = {'opt': 'active'} + grid.showProgress({type: 'icon'}) + + webix.ajax().get('/tickets', filters, { + error: function(text, data, xhr) { + msg_error('Error al consultar') + }, + success: function(text, data, xhr) { + var values = data.json(); + grid.clearAll(); + if (values.ok){ + grid.parse(values.rows, 'json') + } + } + }) +} + + function configuracion_inicial_ticket_to_invoice(){ - //~ get_active_tickets() + var grid = $$('grid_tickets_active') + var gridt = $$('grid_tickets_invoice') + var form = $$('form_ticket_invoice') + + get_active_tickets(grid) + form.setValues({id_partner: 0, lbl_tclient: 'Ninguno'}) + gridt.attachEvent('onAfterAdd', function(id, index){ + gridt.adjustColumn('index') + gridt.adjustColumn('folio', 'all') + gridt.adjustColumn('fecha', 'all') + }); + gridt.clearAll() } @@ -416,6 +453,165 @@ function chk_is_invoice_day_change(new_value, old_value){ } +function send_timbrar_invoice(id){ + webix.ajax().get('/values/timbrar', {id: id}, function(text, data){ + var values = data.json() + if(values.ok){ + msg_ok(values.msg) + }else{ + webix.alert({ + title: 'Error al Timbrar', + text: values.msg, + type: 'alert-error' + }) + } + }) + +} + + +function save_ticket_to_invoice(data){ + webix.ajax().sync().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) + send_timbrar_invoice(values.id) + $$('multi_tickets').setValue('tickets_home') + }else{ + msg_error(values.msg) + } + } + }) +} + + function cmd_new_invoice_from_ticket_click(){ - showvar('ok') + var form = this.getFormView(); + var chk = $$('chk_is_invoice_day') + var grid = $$('grid_tickets_invoice') + var values = form.getValues() + var tickets = [] + + if(!chk.getValue()){ + if(values.id_partner == 0){ + webix.UIManager.setFocus('tsearch_client_name') + msg = 'Selecciona un cliente' + msg_error(msg) + return false + } + } + + if(!grid.count()){ + msg = 'Agrega al menos un ticket a facturar' + msg_error(msg) + return false + } + + grid.eachRow(function(row){ + tickets.push(row) + }) + + var data = new Object() + data['client'] = values.id_partner + data['tickets'] = tickets + data['is_invoice_day'] = chk.getValue() + data['opt'] = 'invoice' + + msg = 'Todos los datos son correctos.

¿Estás seguro de generar esta factura?' + webix.confirm({ + title: 'Generar Factura', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + save_ticket_to_invoice(data) + } + } + }) +} + + +function grid_tickets_active_double_click(id, e, node){ + this.move(id.row, -1, $$('grid_tickets_invoice')) +} + + +function grid_tickets_invoice_double_click(id, e, node){ + this.move(id.row, -1, $$('grid_tickets_active')) +} + + +function cmd_move_tickets_right_click(){ + $$('grid_tickets_active').eachRow( + function(row){ + this.copy(row, -1, $$('grid_tickets_invoice')) + } + ) + $$('grid_tickets_active').clearAll() +} + + +function cmd_move_tickets_left_click(){ + $$('grid_tickets_invoice').eachRow( + function(row){ + this.copy(row, -1, $$('grid_tickets_active')) + } + ) + $$('grid_tickets_invoice').clearAll() +} + + +function ticket_set_client(row){ + var form = $$('form_ticket_invoice') + var html = '' + form.setValues({ + id_partner: row.id, + tsearch_client_key: '', + tsearch_client_name: ''}, true) + html += row.nombre + ' (' + row.rfc + ')' + $$('lbl_tclient').setValue(html) +} + + +function ticket_search_client_by_id(id){ + webix.ajax().get('/values/client', {'id': id}, { + error: function(text, data, xhr) { + msg_error('Error al consultar') + }, + success: function(text, data, xhr){ + var values = data.json() + if (values.ok){ + ticket_set_client(values.row) + }else{ + msg = 'No se encontró un cliente con la clave: ' + id + msg_error(msg) + } + } + }) + +} + + +function tsearch_client_key_press(code, e){ + var value = this.getValue() + if(code == 13 && value.length > 0){ + var id = parseInt(value, 10) + if (isNaN(id)){ + msg_error('Captura una clave válida') + }else{ + ticket_search_client_by_id(id) + } + } +} + + +function grid_ticket_clients_found_click(obj){ + ticket_set_client(obj) } \ No newline at end of file diff --git a/source/static/js/ui/tickets.js b/source/static/js/ui/tickets.js index 5d2df46..609a847 100644 --- a/source/static/js/ui/tickets.js +++ b/source/static/js/ui/tickets.js @@ -200,10 +200,10 @@ var toolbar_ticket_invoice = {view: 'toolbar', elements: [{}, {}]} -var tsuggest_partners = { +var ticket_suggest_partners = { view: 'gridsuggest', - id: 'grid_tclients_found', - name: 'grid_tclients_found', + id: 'grid_ticket_clients_found', + name: 'grid_ticket_clients_found', body: { autoConfig: false, header: false, @@ -233,7 +233,7 @@ var ticket_search_client = {cols: [{rows: [ placeholder:'Presiona ENTER para buscar'}, {view: 'search', id: 'tsearch_client_name', name: 'tsearch_client_name', label: 'por Nombre o RFC', - labelPosition: 'top', suggest: tsuggest_partners, + labelPosition: 'top', suggest: ticket_suggest_partners, placeholder: 'Captura al menos tres letras'}, ]}, {cols: [ @@ -247,17 +247,17 @@ var ticket_search_client = {cols: [{rows: [ var grid_tickets_active_cols = [ - {id: 'index', header: '#', adjust: 'data', css: 'right', - footer: {content: 'countRows', colspan: 3, css: 'right'}}, + {id: 'index', header: '#', adjust: 'data', css: 'right'}, {id: "id", header:"ID", hidden:true}, {id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data", - sort:"string", hidden: true}, + sort: "string", hidden: true}, {id: 'folio', header: ['Folio', {content: 'numberFilter'}], adjust: 'header', - sort: 'int', css: 'right', footer: {text: 'Tickets', colspan: 3}}, - {id: "fecha", header: ["Fecha y Hora"], - adjust: "data", sort: "string"}, - {id: 'total', header: 'Total', width: 150,sort: 'int', - format: webix.i18n.priceFormat, css: 'right'}, + sort: 'int', css: 'right', footer: {content: 'countRows', css: 'right'}}, + {id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort: "string", + footer: 'Tickets'}, + {id: 'total', header: 'Total', width: 150,sort: 'int', css: 'right', + format: webix.i18n.priceFormat, footer: {content: 'summColumn', + css: 'right'}}, ] @@ -267,6 +267,7 @@ var grid_tickets_active = { select: 'row', adjust: true, footer: true, + drag: true, resizeColumn: true, headermenu: true, columns: grid_tickets_active_cols, @@ -281,17 +282,17 @@ var grid_tickets_active = { var grid_tickets_invoice_cols = [ - {id: 'index', header: '#', adjust: 'data', css: 'right', - footer: {content: 'countRows', colspan: 3, css: 'right'}}, + {id: 'index', header: '#', adjust: 'data', css: 'right'}, {id: "id", header:"ID", hidden:true}, {id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data", - sort:"string", hidden: true}, - {id: 'folio', header: 'Folio', adjust: 'header', sort: 'int', - css: 'right', footer: {text: 'Tickets', colspan: 3}}, - {id: "fecha", header: ["Fecha y Hora"], - adjust: "data", sort: "string"}, - {id: 'total', header: 'Total', width: 150,sort: 'int', - format: webix.i18n.priceFormat, css: 'right'}, + sort: "string", hidden: true}, + {id: 'folio', header: ['Folio', {content: 'numberFilter'}], adjust: 'header', + sort: 'int', css: 'right', footer: {content: 'countRows', css: 'right'}}, + {id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort: "string", + footer: 'Tickets'}, + {id: 'total', header: 'Total', width: 150,sort: 'int', css: 'right', + format: webix.i18n.priceFormat, footer: {content: 'summColumn', + css: 'right'}}, ] @@ -301,6 +302,7 @@ var grid_tickets_invoice = { select: 'row', adjust: true, footer: true, + drag: true, resizeColumn: true, headermenu: true, columns: grid_tickets_invoice_cols, @@ -321,8 +323,13 @@ var controls_ticket_to_invoice = [ ticket_search_client, {minHeight: 5, maxHeight: 5}, {cols:[ - {rows: [{view: 'label', label: 'Tickets sin facturar', height: 30, align: 'left'}, grid_tickets_active]}, - {minWidth: 10, maxWidth: 10}, + {rows: [{view: 'label', label: 'Tickets sin facturar', height: 30, + align: 'left'}, + grid_tickets_active]}, + {rows:[{}, + {view: 'button', id: 'cmd_move_tickets_right', label: '->', autowidth: true}, + {view: 'button', id: 'cmd_move_tickets_left', label: '<-', autowidth: true}, + {}]}, {rows: [{view: 'label', label: 'Tickets a facturar', height: 30, align: 'left'}, grid_tickets_invoice]}, ]}, {minHeight: 20, maxHeight: 20},