diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd8eb4..a0602ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +v 1.12.0 [31-ago-2018] +---------------------- + - Soporte para facturas (complemento) de pago. + +* IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal. + +``` +git pull origin master + +cd source/app/models + +python main.py -m +``` + v 1.11.1 [21-ago-2018] ---------------------- - Fix - Quitar columna en tabla facturaspagos. diff --git a/VERSION b/VERSION index 720c738..0eed1a2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.11.1 +1.12.0 diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index 0e93025..2da8ff7 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -99,6 +99,7 @@ class CFDI(object): self._donativo = False self._ine = False self._edu = False + self._pagos = False self._is_nomina = False self.error = '' @@ -149,6 +150,7 @@ class CFDI(object): if datos['complementos']: if 'ine' in datos['complementos']: self._ine = True + self._pagos = datos['complementos'].get('pagos', False) if 'nomina' in datos: self._is_nomina = True @@ -427,7 +429,6 @@ class CFDI(object): complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre)) pre = 'pago10' datos = datos.pop('pagos') - pago = datos.pop('pago') relacionados = datos.pop('relacionados') attributes = {} @@ -437,11 +438,11 @@ class CFDI(object): 'http://www.sat.gob.mx/Pagos ' \ 'http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd' attributes.update(datos) + pagos = ET.SubElement( complemento, '{}:Pagos'.format(pre), attributes) - node_pago = ET.SubElement(pagos, '{}:Pago'.format(pre), pago) - + node_pago = ET.SubElement(pagos, '{}:Pago'.format(pre), datos) for row in relacionados: ET.SubElement(node_pago, '{}:DoctoRelacionado'.format(pre), row) diff --git a/source/app/models/main.py b/source/app/models/main.py index abeafb5..717c00a 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -35,7 +35,7 @@ from settings import log, DEBUG, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \ CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \ DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \ - DEFAULT_SERIE_CFDIPAY + DEFAULT_SERIE_CFDIPAY, DEFAULT_TYPE_CFDIPAY FORMAT = '{0:.2f}' @@ -897,6 +897,10 @@ class Certificado(BaseModel): def __str__(self): return self.serie + @classmethod + def get_cert(cls, is_fiel=False): + return Certificado.get(Certificado.es_fiel==is_fiel) + @classmethod def get_data(cls): obj = cls.get_(cls) @@ -5248,7 +5252,9 @@ class CfdiPagos(BaseModel): folio = IntegerField(default=0) fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) fecha_timbrado = DateTimeField(null=True) + tipo_comprobante = TextField(default=DEFAULT_TYPE_CFDIPAY) lugar_expedicion = TextField(default='') + regimen_fiscal = TextField(default='') tipo_relacion = TextField(default='') uuid_relacionado = UUIDField(null=True) xml = TextField(default='') @@ -5303,6 +5309,7 @@ class CfdiPagos(BaseModel): partner = partner[0] partner_name = related[0].factura.cliente.nombre + regimen_fiscal = related[0].factura.regimen_fiscal filters = ( (CfdiPagos.movimiento==id_mov) & @@ -5310,9 +5317,14 @@ class CfdiPagos(BaseModel): ) previous = CfdiPagos.select().where(filters) if previous: - msg = 'Hay una factura activa, es necesario cancelarla primero' - data = {'ok': False, 'msg': msg} - return data + previous = previous[0] + if previous.uuid: + msg = 'Hay una factura activa, es necesario cancelarla primero' + data = {'ok': False, 'msg': msg} + return data + else: + data = {'ok': True, 'new': False} + return data emisor = Emisor.select()[0] serie = Configuracion.get_('txt_config_cfdipay_serie') or DEFAULT_SERIE_CFDIPAY @@ -5322,33 +5334,165 @@ class CfdiPagos(BaseModel): fields['serie'] = serie fields['folio'] = self._get_folio(self, serie) fields['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal + fields['regimen_fiscal'] = regimen_fiscal - # ~ with database_proxy.atomic() as txn: - # ~ obj = CfdiPagos.create(**fields) + with database_proxy.atomic() as txn: + obj = CfdiPagos.create(**fields) - # ~ row = { - # ~ 'id': obj.id, - # ~ 'serie': obj.serie, - # ~ 'folio': obj.folio, - # ~ 'uuid': obj.uuid, - # ~ 'fecha': obj.fecha, - # ~ 'tipo_comprobante': 'P', - # ~ 'estatus': obj.estatus, - # ~ 'cliente': partner_name, - # ~ } row = { - 'id': 1, - 'serie': 'FP', - 'folio': 1, - 'uuid': '', - 'fecha': util.now(), - 'tipo_comprobante': 'P', - 'estatus': 'Timbrada', + 'id': obj.id, + 'serie': obj.serie, + 'folio': obj.folio, + 'uuid': obj.uuid, + 'fecha': obj.fecha, + 'tipo_comprobante': obj.tipo_comprobante, + 'estatus': obj.estatus, 'cliente': partner_name, } - data = {'ok': True, 'row': row} + data = {'ok': True, 'row': row, 'new': True} return data + def _get_related_xml(self, id_mov): + filters = (FacturasPagos.movimiento==id_mov) + related = tuple(FacturasPagos.select( + Facturas.uuid.alias('IdDocumento'), + Facturas.serie.alias('Serie'), + Facturas.folio.alias('Folio'), + Facturas.moneda.alias('MonedaDR'), + Facturas.tipo_cambio.alias('TipoCambioDR'), + Facturas.metodo_pago.alias('MetodoDePagoDR'), + FacturasPagos.numero.alias('NumParcialidad'), + FacturasPagos.saldo_anterior.alias('ImpSaldoAnt'), + FacturasPagos.importe.alias('ImpPagado'), + FacturasPagos.saldo.alias('ImpSaldoInsoluto'), + ).join(Facturas).switch(FacturasPagos) + .where(filters) + .dicts()) + + for r in related: + r['IdDocumento'] = str(r['IdDocumento']) + r['Folio'] = str(r['Folio']) + r['NumParcialidad'] = str(r['NumParcialidad']) + r['TipoCambioDR'] = FORMAT.format(r['TipoCambioDR']) + r['ImpSaldoAnt'] = FORMAT.format(r['ImpSaldoAnt']) + r['ImpPagado'] = FORMAT.format(r['ImpPagado']) + r['ImpSaldoInsoluto'] = FORMAT.format(r['ImpSaldoInsoluto']) + + return related + + def _generate_xml(self, invoice, auth): + emisor = Emisor.select()[0] + cert = Certificado.get_cert() + + cfdi = {} + related = {} + cfdi['Serie'] = invoice.serie + cfdi['Folio'] = str(invoice.folio) + cfdi['Fecha'] = invoice.fecha.isoformat()[:19] + cfdi['NoCertificado'] = cert.serie + cfdi['Certificado'] = cert.cer_txt + cfdi['SubTotal'] = '0' + cfdi['Moneda'] = 'XXX' + cfdi['Total'] = '0' + cfdi['TipoDeComprobante'] = invoice.tipo_comprobante + cfdi['LugarExpedicion'] = invoice.lugar_expedicion + + if invoice.tipo_relacion: + related = { + 'tipo': invoice.tipo_relacion, + 'cfdis': (invoice.uuid_relacionado,), + } + + emisor = { + 'Rfc': emisor.rfc, + 'Nombre': emisor.nombre, + 'RegimenFiscal': invoice.regimen_fiscal, + } + + receptor = { + 'Rfc': invoice.socio.rfc, + 'Nombre': invoice.socio.nombre, + 'UsoCFDI': 'P01', + } + if invoice.socio.tipo_persona == 4: + if invoice.socio.pais: + receptor['ResidenciaFiscal'] = invoice.socio.pais + if invoice.socio.id_fiscal: + receptor['NumRegIdTrib'] = invoice.socio.id_fiscal + + conceptos = ({ + 'ClaveProdServ': '84111506', + 'Cantidad': '1', + 'ClaveUnidad': 'ACT', + 'Descripcion': 'Pago', + 'ValorUnitario': '0', + 'Importe': '0', + },) + + impuestos = {} + + mov = invoice.movimiento + pagos = { + 'FechaPago': mov.fecha.isoformat()[:19], + 'FormaDePagoP': mov.forma_pago.key, + 'MonedaP': mov.cuenta.moneda.key, + 'Monto': FORMAT.format(mov.deposito), + 'relacionados': self._get_related_xml(self, invoice.movimiento), + } + + complementos = {'pagos': pagos} + data = { + 'comprobante': cfdi, + 'relacionados': related, + 'emisor': emisor, + 'receptor': receptor, + 'conceptos': conceptos, + 'impuestos': impuestos, + 'donativo': {}, + 'edu': False, + 'complementos': complementos, + } + return util.make_xml(data, cert, auth) + + def _stamp(self, values): + id_mov = int(values['id_mov']) + + auth = Emisor.get_auth() + filters = ( + (CfdiPagos.movimiento==id_mov) & + (CfdiPagos.uuid.is_null(True)) + ) + obj = CfdiPagos.get(filters) + obj.xml = self._generate_xml(self, obj, auth) + obj.estatus = 'Generada' + obj.save() + # ~ result = util.timbra_xml(obj.xml, auth) + data = {'ok': True, 'row': {}} + return data + + def _get_related(self, values): + id_mov = int(values['id_mov']) + filters = ( + (CfdiPagos.movimiento==id_mov) + ) + rows = tuple(CfdiPagos.select( + CfdiPagos.id, + CfdiPagos.serie, + CfdiPagos.folio, + CfdiPagos.uuid, + CfdiPagos.fecha, + CfdiPagos.tipo_comprobante, + CfdiPagos.estatus, + Socios.nombre.alias('cliente'), + ).join(Socios).switch(CfdiPagos) + .where(filters).dicts()) + return {'ok': True, 'rows': rows} + + @classmethod + def get_values(cls, values): + opt = values.pop('opt') + return getattr(cls, '_get_{}'.format(opt))(cls, values) + class PreFacturasImpuestos(BaseModel): factura = ForeignKeyField(PreFacturas) @@ -7627,12 +7771,16 @@ def _migrate_tables(): serie = TextField(default='') folio = IntegerField(default=0) lugar_expedicion = TextField(default='') + regimen_fiscal = TextField(default='') + tipo_comprobante = TextField(default=DEFAULT_TYPE_CFDIPAY) error = TextField(default='') tipo_relacion = TextField(default='') uuid_relacionado = UUIDField(null=True) migrations.append(migrator.add_column('cfdipagos', 'serie', serie)) migrations.append(migrator.add_column('cfdipagos', 'folio', folio)) migrations.append(migrator.add_column('cfdipagos', 'lugar_expedicion', lugar_expedicion)) + migrations.append(migrator.add_column('cfdipagos', 'regimen_fiscal', regimen_fiscal)) + migrations.append(migrator.add_column('cfdipagos', 'tipo_comprobante', tipo_comprobante)) migrations.append(migrator.add_column('cfdipagos', 'error', error)) migrations.append(migrator.add_column('cfdipagos', 'tipo_relacion', tipo_relacion)) migrations.append(migrator.add_column('cfdipagos', 'uuid_relacionado', uuid_relacionado)) diff --git a/source/app/settings.py b/source/app/settings.py index 0d1ed66..1914484 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -47,7 +47,7 @@ except ImportError: DEBUG = DEBUG -VERSION = '1.11.1' +VERSION = '1.12.0' EMAIL_SUPPORT = ('soporte@empresalibre.net',) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) @@ -155,6 +155,7 @@ IMPUESTOS = { DEFAULT_SAT_PRODUCTO = '01010101' DEFAULT_SERIE_TICKET = 'T' DEFAULT_SERIE_CFDIPAY = 'FP' +DEFAULT_TYPE_CFDIPAY = 'P' DIR_FACTURAS = 'facturas' USAR_TOKEN = False CANCEL_SIGNATURE = False diff --git a/source/static/js/controller/bancos.js b/source/static/js/controller/bancos.js index a683c70..5e85daf 100644 --- a/source/static/js/controller/bancos.js +++ b/source/static/js/controller/bancos.js @@ -728,6 +728,20 @@ function set_data_pay(row){ } }) + $$('grid_cfdi_pay').clearAll() + webix.ajax().get('/cfdipay', {'opt': 'related', 'id_mov': row.id}, { + error:function(text, data, XmlHttpRequest){ + msg = 'Ocurrio un error, consulta a soporte técnico' + msg_error(msg) + }, + success:function(text, data, XmlHttpRequest){ + var values = data.json() + if(values.ok){ + $$('grid_cfdi_pay').parse(values.rows, 'json') + } + } + }) + } @@ -759,10 +773,50 @@ function validate_cfdi_pay(form){ return false } + var grid = $$('grid_pay_related') + if(grid.count() == 0){ + msg_error('El depósito no tiene facturas relacionadas') + return false + } + return true } +function update_grid_cfdi_pay(row){ + var g = $$('grid_cfdi_pay') + + g.add(result.row) + if (g.count() == 1){ + g.adjustColumn('index') + g.adjustColumn('serie') + g.adjustColumn('folio') + g.adjustColumn('fecha') + g.adjustColumn('cliente') + g.adjustColumn('xml') + g.adjustColumn('pdf') + g.adjustColumn('email') + } +} + +function send_stamp_cfdi_pay(id_mov){ + var data = {'opt': 'stamp', 'id_mov': id_mov} + + webix.ajax().sync().post('cfdipay', data, { + error:function(text, data, XmlHttpRequest){ + msg = 'Ocurrio un error, consulta a soporte técnico' + msg_error(msg) + }, + success:function(text, data, XmlHttpRequest){ + result = data.json(); + if(result.ok){ + msg = 'Factura timbrada correctamente' + msg_ok(msg) + } + } + }) +} + function save_cfdi_pay(form){ var values = form.getValues() var data = {'opt': 'new', 'id_mov': values.id_mov} @@ -775,8 +829,13 @@ function save_cfdi_pay(form){ success:function(text, data, XmlHttpRequest){ result = data.json(); if(result.ok){ - msg_ok('Factura guardada correctamente
Enviando a timbrar...') - $$('grid_cfdi_pay').add(result.row) + if(result.new){ + msg_ok('Factura guardada correctamente
Enviando a timbrar...') + update_grid_cfdi_pay(result.row) + }else{ + msg_ok('Enviando a timbrar...') + } + send_stamp_cfdi_pay(values.id_mov) }else{ msg_error(result.msg) } diff --git a/source/static/js/ui/bancos.js b/source/static/js/ui/bancos.js index 32cd3be..78611c3 100644 --- a/source/static/js/ui/bancos.js +++ b/source/static/js/ui/bancos.js @@ -41,7 +41,7 @@ var toolbar_movimientos_banco = [ {view: 'button', id: 'cmd_agregar_deposito', label: 'Depósito', type: 'iconButton', autowidth: true, icon: 'plus'}, {}, - {view: 'button', id: 'cmd_complemento_pago', label: 'Complemento de Pago', + {view: 'button', id: 'cmd_complemento_pago', label: 'Factura de Pago', type: 'iconButton', autowidth: true, icon: 'file-code-o'}, {}, {view: 'button', id: 'cmd_cancelar_movimiento', label: 'Cancelar', @@ -118,8 +118,7 @@ var grid_cfdi_pago_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"], adjust: "data", - sort: 'string', template: '{common.subrow()} #serie#'}, + {id: 'serie', header: ["Serie"], adjust: "data", sort: 'string'}, {id: 'folio', header: ['Folio'], adjust: 'data', sort: 'int', css: 'right', footer: {text: 'Facturas', colspan: 3}}, {id: "uuid", header: ["UUID"], adjust: "data",