From d61e0e8d8ec82085bf889933e7e83a29db148ba3 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Wed, 8 Nov 2017 23:47:15 -0600 Subject: [PATCH] XML con CFDI relacionados --- source/app/controllers/cfdi_xml.py | 21 ++- source/app/models/db.py | 3 + source/app/models/main.py | 107 +++++++++++- source/db/valores_iniciales.json | 2 +- source/static/js/controller/invoices.js | 209 ++++++++++++++++++++++-- source/static/js/controller/util.js | 1 + source/static/js/ui/invoices.js | 170 +++++++++++++++++-- 7 files changed, 470 insertions(+), 43 deletions(-) diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index 6cef536..fb61c4b 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -59,6 +59,7 @@ class CFDI(object): return '' self._comprobante(datos['comprobante']) + self._relacionados(datos['relacionados']) self._emisor(datos['emisor']) self._receptor(datos['receptor']) self._conceptos(datos['conceptos']) @@ -107,10 +108,6 @@ class CFDI(object): attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] attributes.update(datos) - #~ if DEBUG: - #~ attributes['Fecha'] = self._now() - #~ attributes['NoCertificado'] = CERT_NUM - if not 'Version' in attributes: attributes['Version'] = self._sat_cfdi['version'] if not 'Fecha' in attributes: @@ -119,10 +116,20 @@ class CFDI(object): self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes) return - def _emisor(self, datos): - #~ if DEBUG: - #~ datos['Rfc'] = RFC_TEST + def _relacionados(self, datos): + if not datos['tipo'] or not datos['cfdis']: + return + node_name = '{}:CfdiRelacionados'.format(self._pre) + value = {'TipoRelacion': datos['tipo']} + node = ET.SubElement(self._cfdi, node_name, value) + for uuid in datos['cfdis']: + node_name = '{}:CfdiRelacionado'.format(self._pre) + value = {'UUID': uuid} + ET.SubElement(node, node_name, value) + return + + def _emisor(self, datos): node_name = '{}:Emisor'.format(self._pre) emisor = ET.SubElement(self._cfdi, node_name, datos) return diff --git a/source/app/models/db.py b/source/app/models/db.py index 2b2efc2..c53fdfc 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -64,6 +64,9 @@ class StorageEngine(object): def _get_formapago(self, values): return main.SATFormaPago.get_activos(values) + def _get_tiporelacion(self, values): + return main.SATTipoRelacion.get_activos(values) + def _get_condicionespago(self, values): return main.CondicionesPago.get_() diff --git a/source/app/models/main.py b/source/app/models/main.py index 7457729..622c35b 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -4,7 +4,7 @@ import sqlite3 import click from peewee import * from playhouse.fields import PasswordField, ManyToManyField -from playhouse.shortcuts import case, SQL +from playhouse.shortcuts import case, SQL, cast if __name__ == '__main__': @@ -704,6 +704,19 @@ class SATTipoRelacion(BaseModel): def __str__(self): return 'Tipo de relación: ({}) {}'.format(self.key, self.name) + @classmethod + def get_activos(cls, values): + field = SATTipoRelacion.id + if values: + field = SATTipoRelacion.key.alias('id') + rows = (SATTipoRelacion + .select(field, SATTipoRelacion.name.alias('value')) + .where(SATTipoRelacion.activo==True) + .dicts() + ) + return ({'id': '-', 'value': ''},) + tuple(rows) + + class SATUsoCfdi(BaseModel): key = TextField(index=True, unique=True) @@ -1336,8 +1349,72 @@ class Facturas(BaseModel): msg = 'Factura enviada correctamente' return {'ok': True, 'msg': msg} + def _get_filter_folios(self, values): + if not 'folio' in values: + return '' + + folios = values['folio'].split('-') + if len(folios) == 1: + try: + folio1 = int(folios[0]) + except ValueError: + return '' + + folio2 = folio1 + else: + try: + folio1 = int(folios[0]) + folio2 = int(folios[1]) + except ValueError: + return '' + + return (Facturas.folio.between(folio1, folio2)) + + def _get_opt(self, values): + cfdis = util.loads(values['cfdis']) + + if values['year'] == '-1': + fy = (Facturas.fecha.year > 0) + else: + fy = (Facturas.fecha.year == int(values['year'])) + if values['month'] == '-1': + fm = (Facturas.fecha.month > 0) + else: + fm = (Facturas.fecha.month == int(values['month'])) + + if values['opt'] == 'relacionados': + folios = self._get_filter_folios(self, values) + uuid = values.get('uuid', '') + if uuid: + f_uuid = (cast(Facturas.uuid, 'text').contains(uuid)) + cliente = (Facturas.cliente == int(values['id_cliente'])) + if cfdis: + f_ids = (Facturas.id.not_in(cfdis)) + else: + f_ids = (Facturas.id > 0) + + if folios: + filters = (fy & fm & folios & cliente & f_ids) + elif uuid: + filters = (fy & fm & f_uuid & cliente & f_ids) + else: + filters = (fy & fm & cliente & f_ids) + + rows = tuple(Facturas + .select(Facturas.id, Facturas.serie, Facturas.folio, + Facturas.uuid, Facturas.fecha, Facturas.tipo_comprobante, + Facturas.estatus, Facturas.total_mn) + .where(filters).dicts() + ) + + return {'ok': True, 'rows': rows} + @classmethod def get_(cls, values): + opt = values.get('opt', '') + if opt: + return cls._get_opt(cls, values) + if 'start' in values: filters = Facturas.fecha.between( util.get_date(values['start']), @@ -1469,10 +1546,19 @@ class Facturas(BaseModel): } return data + def _guardar_relacionados(self, invoice, relacionados): + for cfdi in relacionados: + data = { + 'factura': invoice, + 'factura_origen': cfdi, + } + FacturasRelacionadas.create(**data) + return + @classmethod def add(cls, values): - #~ print ('VALUES', values) productos = util.loads(values.pop('productos')) + relacionados = util.loads(values.pop('relacionados')) emisor = Emisor.select()[0] values['folio'] = cls._get_folio(cls, values['serie']) @@ -1482,6 +1568,7 @@ class Facturas(BaseModel): with database_proxy.atomic() as txn: obj = Facturas.create(**values) totals = cls._calculate_totals(cls, obj, productos) + cls._guardar_relacionados(cls, obj, relacionados) obj.subtotal = totals['subtotal'] obj.total_trasladados = totals['total_trasladados'] obj.total_retenciones = totals['total_retenciones'] @@ -1508,6 +1595,7 @@ class Facturas(BaseModel): emisor = Emisor.select()[0] certificado = Certificado.select()[0] comprobante = {} + relacionados = {} if invoice.serie: comprobante['Serie'] = invoice.serie if invoice.condiciones_pago: @@ -1529,6 +1617,11 @@ class Facturas(BaseModel): comprobante['TipoDeComprobante'] = invoice.tipo_comprobante comprobante['MetodoPago'] = invoice.metodo_pago comprobante['LugarExpedicion'] = invoice.lugar_expedicion + if invoice.tipo_relacion: + relacionados = { + 'tipo': invoice.tipo_relacion, + 'cfdis': FacturasRelacionadas.get_(invoice), + } emisor = { 'Rfc': emisor.rfc, @@ -1623,6 +1716,7 @@ class Facturas(BaseModel): data = { 'comprobante': comprobante, + 'relacionados': relacionados, 'emisor': emisor, 'receptor': receptor, 'conceptos': conceptos, @@ -2012,6 +2106,15 @@ class FacturasRelacionadas(BaseModel): class Meta: order_by = ('factura',) + @classmethod + def get_(cls, invoice): + query = (FacturasRelacionadas + .select() + .where(FacturasRelacionadas.factura==invoice) + ) + return [str(r.factura_origen.uuid) for r in query] + + class PreFacturasRelacionadas(BaseModel): factura = ForeignKeyField(PreFacturas, related_name='original') factura_origen = ForeignKeyField(PreFacturas, related_name='relacion') diff --git a/source/db/valores_iniciales.json b/source/db/valores_iniciales.json index 53adaed..322bcef 100644 --- a/source/db/valores_iniciales.json +++ b/source/db/valores_iniciales.json @@ -36,7 +36,7 @@ {"key": "04", "name": "Sustitución de los CFDI previos", "activo": true, "default": true}, {"key": "05", "name": "Traslados de mercancias facturados previamente", "activo": true}, {"key": "06", "name": "Factura generada por los traslados previos", "activo": true}, - {"key": "07", "name": "Actividad", "CFDI por aplicación de anticipo": true} + {"key": "07", "name": "CFDI por aplicación de anticipo", "activo": true} ] }, { diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 5c757d4..9bf0193 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -1,6 +1,8 @@ var query = [] var grid = null var msg = '' +var result = false +var tipo_relacion = '' function get_condicion_pago(){ @@ -108,6 +110,7 @@ function cmd_new_invoice_click(id, e, node){ grid.clearAll() grid_totals.clearAll() grid_totals.add({id: 1, concepto: 'SubTotal', importe: 0}) + $$('cmd_cfdi_relacionados').disable() $$('multi_invoices').setValue('invoices_new') form.focus('search_client_name') } @@ -148,7 +151,7 @@ function cmd_delete_invoice_click(id, e, node){ return } - var msg = '¿Estás seguro de eliminar la siguiente Factura?

' + msg = '¿Estás seguro de eliminar la siguiente Factura?

' msg += '(' + row['folio'] + ') ' + row['cliente'] msg += '

ESTA ACCIÓN NO SE PUEDE DESHACER' webix.confirm({ @@ -228,7 +231,6 @@ function validate_invoice(values){ return false } - return true } @@ -325,17 +327,13 @@ function save_preinvoice(data){ } -function cmd_timbrar_click(id, e, node){ - var form = this.getFormView(); - - if(!form.validate()) { - webix.message({type:'error', text:'Valores inválidos'}) - return - } - - var values = form.getValues(); - if(!validate_invoice(values)){ - return +function guardar_y_timbrar(values){ + query = table_relaciones.chain().data() + var ids = [] + if(query.length > 0){ + for (i = 0; i < query.length; i++) { + ids.push(query[i]['id']) + } } var rows = grid.data.getRange() @@ -360,13 +358,53 @@ function cmd_timbrar_click(id, e, node){ data['metodo_pago'] = $$('lst_metodo_pago').getValue() data['uso_cfdi'] = $$('lst_uso_cfdi').getValue() data['regimen_fiscal'] = $$('lst_regimen_fiscal').getValue() + data['relacionados'] = ids + data['tipo_relacion'] = tipo_relacion if(!save_invoice(data)){ return } - form.setValues({id_partner: 0, lbl_partner: 'Ninguno'}) + table_relaciones.clear() + tipo_relacion = '' + $$('form_invoice').setValues({id_partner: 0, lbl_partner: 'Ninguno'}) $$('multi_invoices').setValue('invoices_home') + +} + + +function cmd_timbrar_click(id, e, node){ + var form = this.getFormView(); + + if(!form.validate()) { + webix.message({type:'error', text:'Valores inválidos'}) + return + } + + var values = form.getValues() + if(!validate_invoice(values)){ + return + } + + query = table_relaciones.chain().data() + msg = '¿Todos los datos son correctos?

' + if(query.length > 0){ + msg += 'La factura tiene CFDI relacionados

' + } + msg += '¿Estás seguro de timbrar esta factura?' + + webix.confirm({ + title: 'Timbrar Factura', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + guardar_y_timbrar(values) + } + } + }) } @@ -403,6 +441,7 @@ function set_client(row){ forma_pago: row.forma_pago, uso_cfdi: row.uso_cfdi}, true) html += row.nombre + ' (' + row.rfc + ')' $$('lbl_client').setValue(html) + $$('cmd_cfdi_relacionados').enable() form.focus('search_product_id') } @@ -874,6 +913,7 @@ function reset_invoice(){ table_pt.clear() table_totals.clear() + $$('cmd_cfdi_relacionados').disable() form.focus('search_client_name') } @@ -1130,6 +1170,143 @@ function grid_preinvoices_click(id, e, node){ } -function cmd_cfdi_relacionados_click(){ - show('CFDI Relacionados, en desarrollo') +function get_facturas_por_cliente(){ + var values = $$('form_invoice').getValues() + var id = values.id_partner + + var y = $$('filter_cfdi_year').getValue() + var m = $$('filter_cfdi_month').getValue() + + var ids = [] + var rows = $$('grid_relacionados').data.getRange() + for (i = 0; i < rows.length; i++) { + ids.push(rows[i]['id']) + } + + filters = { + 'year': y, + 'month': m, + 'id_cliente': id, + 'cfdis': ids, + 'folio': $$('filter_cfdi_folio').getValue(), + 'uuid': $$('filter_cfdi_uuid').getValue(), + 'opt': 'relacionados' + } + + var grid = $$('grid_cfdi_cliente') + + webix.ajax().get('/invoices', filters, { + error: function(text, data, xhr) { + webix.message({type: 'error', text: 'Error al consultar'}) + }, + success: function(text, data, xhr) { + var values = data.json(); + grid.clearAll(); + if (values.ok){ + grid.parse(values.rows, 'json'); + }; + } + }) } + + +function get_info_cfdi_relacionados(){ + + webix.ajax().get('/values/tiporelacion', {key: true}, function(text, data){ + var values = data.json() + $$('lst_tipo_relacion').getList().parse(values) + $$('lst_tipo_relacion').setValue(tipo_relacion) + }) + + query = table_relaciones.chain().data() + $$('grid_relacionados').parse(query) + get_facturas_por_cliente() +} + + +function cmd_cfdi_relacionados_click(){ + var d = new Date() + ui_invoice.init() + var fy = $$('filter_cfdi_year') + var fm = $$('filter_cfdi_month') + + fy.blockEvent() + fm.blockEvent() + + $$('lbl_cfdi_cliente').setValue($$('lbl_client').getValue()) + data = $$('filter_year').getList().data + fy.getList().data.sync(data) + fy.setValue(d.getFullYear()) + fm.setValue(d.getMonth() + 1) + + fy.unblockEvent() + fm.unblockEvent() + + get_info_cfdi_relacionados() + + $$('win_cfdi_relacionados').show() +} + + +function cmd_limpiar_relacionados_click(){ + msg = '¿Estás seguro de quitar todas las relaciones

' + msg += 'ESTA ACCION NO SE PUEDE DESHACER' + + webix.confirm({ + title: 'Limpiar relaciones', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('lst_tipo_relacion').setValue('') + $$('grid_relacionados').clearAll() + table_relaciones.clear() + tipo_relacion = '' + msg_sucess('Las relaciones han sido eliminadas') + } + } + }) +} + + +function cmd_guardar_relacionados_click(){ + var grid = $$('grid_relacionados') + var value = $$('lst_tipo_relacion').getValue() + + if(value == '' || value == '-'){ + msg_error('Selecciona el tipo de relación') + return + } + + if(grid.count() == 0){ + msg_error('Agrega al menos un CFDI a relacionar') + return + } + + var data = grid.data.getRange() + table_relaciones.clear() + table_relaciones.insert(data) + tipo_relacion = value + msg_sucess('Relaciones guardadas correctamente') +} + + +function cmd_filter_relacionados_click(){ + get_facturas_por_cliente() +} + + +function filter_cfdi_year_change(nv, ov){ + cmd_filter_relacionados_click() +} + + +function filter_cfdi_month_change(nv, ov){ + cmd_filter_relacionados_click() +} + + + + diff --git a/source/static/js/controller/util.js b/source/static/js/controller/util.js index 568d557..f945d78 100644 --- a/source/static/js/controller/util.js +++ b/source/static/js/controller/util.js @@ -12,6 +12,7 @@ var table_pt = db.addCollection('productstaxes') var table_totals = db.addCollection('totals', {unique: ['tax']}) var table_series = db.addCollection('series') var table_usocfdi = db.addCollection('usocfdi') +var table_relaciones = db.addCollection('relaciones') function show(values){ diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 77eda3b..fb2c11f 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -1,4 +1,154 @@ +var months = [ + {id: -1, value: 'Todos'}, + {id: 1, value: 'Enero'}, + {id: 2, value: 'Febrero'}, + {id: 3, value: 'Marzo'}, + {id: 4, value: 'Abril'}, + {id: 5, value: 'Mayo'}, + {id: 6, value: 'Junio'}, + {id: 7, value: 'Julio'}, + {id: 8, value: 'Agosto'}, + {id: 9, value: 'Septiembre'}, + {id: 10, value: 'Octubre'}, + {id: 11, value: 'Noviembre'}, + {id: 12, value: 'Diciembre'}, +] + + +var grid_cfdi_cliente_cols = [ + {id: 'index', header: '#', adjust: 'data', css: 'right', + footer: {content: 'rowCount', colspan: 3, css: 'right'}}, + {id: "id", header:"ID", hidden:true}, + {id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "header", + sort:"string"}, + {id: 'folio', header: ['Folio'], adjust: 'data', sort: 'int', + css: 'right'}, + {id: 'uuid', header: ['UUID', {content: 'textFilter'}], width: 250, + sort: 'string'}, + {id: "fecha", header: ["Fecha y Hora"], width: 150, sort: 'date'}, + {id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}], + adjust: 'header', sort: 'string'}, + {id: "estatus", header: ["Estatus", {content: "selectFilter"}], + adjust: "header", sort:"string"}, + {id: 'total_mn', header: ['Total M.N.'], width: 150, + sort: 'int', format: webix.i18n.priceFormat, css: 'right'}, +] + + +var grid_relacionados_cols = [ + {id: 'index', header: '#', adjust: 'data', css: 'right'}, + {id: "id", header:"ID", hidden:true}, + {id: "serie", header: "Serie", adjust: "header", sort:"string"}, + {id: 'folio', header: 'Folio', adjust: 'data', sort: 'int', css: 'right'}, + {id: 'uuid', header: 'UUID', width: 250, sort: 'string'}, + {id: "fecha", header: "Fecha y Hora", width: 150, sort: 'date'}, + {id: "tipo_comprobante", header: "Tipo", adjust: 'header', sort: 'string'}, + {id: "estatus", header: "Estatus", adjust: "header", sort:"string"}, + {id: 'total_mn', header: ['Total M.N.'], width: 150, + sort: 'int', format: webix.i18n.priceFormat, css: 'right'}, +] + + +var grid_cfdi_cliente = { + view: 'datatable', + id: 'grid_cfdi_cliente', + select: 'row', + autoConfig: false, + adjust: true, + height: 300, + resizeColumn: true, + headermenu: true, + drag: true, + columns: grid_cfdi_cliente_cols, + on:{ + 'data->onStoreUpdated':function(){ + this.data.each(function(obj, i){ + obj.index = i + 1 + }) + } + } +} + + +var grid_relacionados = { + view: 'datatable', + id: 'grid_relacionados', + select: 'row', + autoConfig: false, + adjust: true, + height: 200, + resizeColumn: true, + headermenu: true, + drag: true, + columns: grid_relacionados_cols, + on:{ + 'data->onStoreUpdated':function(){ + this.data.each(function(obj, i){ + obj.index = i + 1 + }) + } + } +} + + +var body_cfdi_relacionados = {rows: [ + {cols: [ + {view: 'label', id: 'lbl_cfdi_title', label: 'Cliente: ', + autowidth: true}, + {view: 'label', id: 'lbl_cfdi_cliente', label: '', align: 'left'}]}, + {view: 'richselect', id: 'lst_tipo_relacion', label: 'Tipo de Relación', + labelWidth: 150, required: true, options: []}, + {minHeight: 10, maxHeight: 10}, + {cols: [ + {view: 'richselect', id: 'filter_cfdi_year', label: 'Año', width: 100, + labelAlign: 'center', labelPosition: 'top', options: []}, + {view: 'richselect', id: 'filter_cfdi_month', label: 'Mes', width: 125, + labelAlign: 'center', labelPosition: 'top', options: months}, + {view: 'text', id: 'filter_cfdi_folio', label: 'Folio', width: 125, + labelAlign: 'center', labelPosition: 'top'}, + {view: 'text', id: 'filter_cfdi_uuid', label: 'UUID', + labelAlign: 'center', labelPosition: 'top'}, + {view: 'icon', id: 'cmd_filter_relacionados', icon: 'filter'}, + ]}, + grid_cfdi_cliente, + {minHeight: 10, maxHeight: 10}, + {view: 'label', label: 'CFDI Relacionados'}, + grid_relacionados, + {minHeight: 10, maxHeight: 10}, + {cols: [{}, + {view: 'button', id: 'cmd_guardar_relacionados', label: 'Relacionar'}, + {view: 'button', id: 'cmd_limpiar_relacionados', label: 'Limpiar'}, + {}]}, + {minHeight: 15, maxHeight: 15}, +]} + + +var ui_invoice = { + init: function(){ + webix.ui({ + view: 'window', + id: 'win_cfdi_relacionados', + autoheight: true, + width: 850, + modal: true, + position: 'center', + head: {view: 'toolbar', + elements: [ + {view: 'label', label: 'CFDI Relacionados'}, + {view: 'icon', icon: 'times-circle', + click: '$$("win_cfdi_relacionados").close()'}, + ] + }, + body: body_cfdi_relacionados, + }) + $$('cmd_guardar_relacionados').attachEvent('onItemClick', cmd_guardar_relacionados_click) + $$('cmd_limpiar_relacionados').attachEvent('onItemClick', cmd_limpiar_relacionados_click) + $$('cmd_filter_relacionados').attachEvent('onItemClick', cmd_filter_relacionados_click) + $$('filter_cfdi_year').attachEvent('onChange', filter_cfdi_year_change) + $$('filter_cfdi_month').attachEvent('onChange', filter_cfdi_month_change) +}} + var toolbar_invoices = [ {view: "button", id: "cmd_new_invoice", label: "Nueva", type: "iconButton", @@ -28,23 +178,6 @@ var toolbar_invoices_generate = {view: 'toolbar', elements: [{}, ]} -var months = [ - {id: -1, value: 'Todos'}, - {id: 1, value: 'Enero'}, - {id: 2, value: 'Febrero'}, - {id: 3, value: 'Marzo'}, - {id: 4, value: 'Abril'}, - {id: 5, value: 'Mayo'}, - {id: 6, value: 'Junio'}, - {id: 7, value: 'Julio'}, - {id: 8, value: 'Agosto'}, - {id: 9, value: 'Septiembre'}, - {id: 10, value: 'Octubre'}, - {id: 11, value: 'Noviembre'}, - {id: 12, value: 'Diciembre'}, -] - - var toolbar_invoices_filter = [ {view: 'richselect', id: 'filter_year', label: 'Año', labelAlign: 'right', labelWidth: 50, width: 150, options: []}, @@ -443,3 +576,6 @@ var app_invoices = { } + + +