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 f01e117..ec369ee 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -1,4 +1,20 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +# ~ Empresa Libre +# ~ Copyright (C) 2016-2018 Mauricio Baeza Servin (web@correolibre.net) +# ~ +# ~ This program is free software: you can redistribute it and/or modify +# ~ it under the terms of the GNU General Public License as published by +# ~ the Free Software Foundation, either version 3 of the License, or +# ~ (at your option) any later version. +# ~ +# ~ This program is distributed in the hope that it will be useful, +# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of +# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# ~ GNU General Public License for more details. +# ~ +# ~ You should have received a copy of the GNU General Public License +# ~ along with this program. If not, see . import datetime from xml.etree import ElementTree as ET @@ -6,8 +22,6 @@ from xml.dom.minidom import parseString from logbook import Logger -#~ from settings import DEBUG - log = Logger('XML') CFDI_ACTUAL = 'cfdi33' @@ -64,6 +78,12 @@ SAT = { 'xmlns': 'http://www.sat.gob.mx/iedu', 'schema': ' http://www.sat.gob.mx/iedu http://www.sat.gob.mx/sitio_internet/cfd/ine/iedu.xsd', }, + 'pagos': { + 'version': '1.0', + 'prefix': 'pago10', + 'xmlns': 'http://www.sat.gob.mx/Pagos', + 'schema': ' http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd', + }, } @@ -79,6 +99,7 @@ class CFDI(object): self._donativo = False self._ine = False self._edu = False + self._pagos = False self._is_nomina = False self.error = '' @@ -129,6 +150,7 @@ class CFDI(object): if datos['complementos']: if 'ine' in datos['complementos']: self._ine = True + self._pagos = bool(datos['complementos'].get('pagos', False)) if 'nomina' in datos: self._is_nomina = True @@ -174,9 +196,15 @@ class CFDI(object): attributes[name] = SAT['nomina']['xmlns'] schema_nomina = SAT['nomina']['schema'] + schema_pagos = '' + if self._pagos: + name = 'xmlns:{}'.format(SAT['pagos']['prefix']) + attributes[name] = SAT['pagos']['xmlns'] + schema_pagos = SAT['pagos']['schema'] + attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \ schema_locales + schema_donativo + schema_ine + schema_edu + \ - schema_nomina + schema_nomina + schema_pagos attributes.update(datos) if not 'Version' in attributes: @@ -403,6 +431,17 @@ class CFDI(object): atributos.update(datos['ine']) ET.SubElement(self._complemento, 'ine:INE', atributos) + if 'pagos' in datos: + datos = datos.pop('pagos') + relacionados = datos.pop('relacionados') + pre = SAT['pagos']['prefix'] + attributes = {'Version': SAT['pagos']['version']} + pagos = ET.SubElement( + self._complemento, '{}:Pagos'.format(pre), attributes) + node_pago = ET.SubElement(pagos, '{}:Pago'.format(pre), datos) + for row in relacionados: + ET.SubElement(node_pago, '{}:DoctoRelacionado'.format(pre), row) + if 'ce' in datos: pre = 'cce11' datos = datos.pop('ce') diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 1003d85..7cea456 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -514,3 +514,31 @@ class AppDocumentos(object): 'attachment; filename={}'.format(file_name)) resp.content_type = content_type resp.status = falcon.HTTP_200 + + +# ~ Revisado +class AppInvoicePay(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_invoicepay(values) + resp.status = falcon.HTTP_200 + + +class AppCfdiPay(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_cfdipay(values) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + values = req.params + req.context['result'] = self._db.cfdipay(values) + resp.status = falcon.HTTP_200 diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 67426ac..f21dc87 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -320,6 +320,9 @@ def get_template_ods(name, default='plantilla_factura.ods'): if is_file(path): return path + if 'pagos' in name: + default='plantilla_pagos.ods' + path = _join(PATH_TEMPLATES, default) if is_file(path): return path @@ -954,6 +957,9 @@ class LIBO(object): self._set_cell('{total_cantidades}', str(self._total_cantidades)) + if self._pagos: + return + cell_title = self._set_cell('{subtotal.titulo}', 'SubTotal') value = data['subtotal'] cell_value = self._set_cell('{subtotal}', value, value=True) @@ -1150,16 +1156,114 @@ class LIBO(object): return + def _cfdipays(self, data): + related = data.pop('related', []) + for k, v in data.items(): + if k.lower() in ('monto',): + self._set_cell('{pago.%s}' % k, v, value=True) + else: + self._set_cell('{pago.%s}' % k, v) + + col1 = [] + col2 = [] + col3 = [] + col4 = [] + col5 = [] + col6 = [] + col7 = [] + col8 = [] + col9 = [] + count = len(related) + for i, doc in enumerate(related): + uuid = doc['IdDocumento'].upper() + serie = doc['Serie'] + folio = doc['Folio'] + metodo_pago = doc['MetodoDePagoDR'] + moneda = doc['MonedaDR'] + parcialidad = doc['NumParcialidad'] + saldo_anterior = doc['ImpSaldoAnt'] + importe_pagado = doc['ImpPagado'] + saldo_insoluto = doc['ImpSaldoInsoluto'] + if i == 0: + cell_1 = self._set_cell('{doc.uuid}', uuid) + cell_2 = self._set_cell('{doc.serie}', serie) + cell_3 = self._set_cell('{doc.folio}', folio) + cell_4 = self._set_cell('{doc.metodopago}', metodo_pago) + cell_5 = self._set_cell('{doc.moneda}', moneda) + cell_6 = self._set_cell('{doc.parcialidad}', parcialidad) + cell_7 = self._set_cell('{doc.saldoanterior}', saldo_anterior, value=True) + cell_8 = self._set_cell('{doc.importepagado}', importe_pagado, value=True) + cell_9 = self._set_cell('{doc.saldoinsoluto}', saldo_insoluto, value=True) + else: + col1.append((uuid,)) + col2.append((serie,)) + col3.append((folio,)) + col4.append((metodo_pago,)) + col5.append((moneda,)) + col6.append((parcialidad,)) + col7.append((float(saldo_anterior),)) + col8.append((float(importe_pagado),)) + col9.append((float(saldo_insoluto),)) + + if count == 1: + return + + count -= 1 + row1 = cell_1.getCellAddress().Row + 1 + row2 = row1 + count - 1 + self._sheet.getRows().insertByIndex(row1, count) + self._copy_paste_rows(cell_1, count) + + # ~ style_7 = self._get_style(cell_7) + # ~ style_8 = self._get_style(cell_8) + # ~ style_9 = self._get_style(cell_9) + + col = cell_1.getCellAddress().Column + target1 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + col = cell_2.getCellAddress().Column + target2 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + col = cell_3.getCellAddress().Column + target3 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + col = cell_4.getCellAddress().Column + target4 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + col = cell_5.getCellAddress().Column + target5 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + col = cell_6.getCellAddress().Column + target6 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + col = cell_7.getCellAddress().Column + target7 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + col = cell_8.getCellAddress().Column + target8 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + col = cell_9.getCellAddress().Column + target9 = self._sheet.getCellRangeByPosition(col, row1, col, row2) + + target1.setFormulaArray(tuple(col1)) + target2.setDataArray(tuple(col2)) + target3.setFormulaArray(tuple(col3)) + target4.setDataArray(tuple(col4)) + target5.setDataArray(tuple(col5)) + target6.setDataArray(tuple(col6)) + target7.setDataArray(tuple(col7)) + target8.setDataArray(tuple(col8)) + target9.setDataArray(tuple(col9)) + + return + def _render(self, data): self._set_search() self._es_pre = data.pop('es_pre', False) self._is_ticket = data.pop('is_ticket', False) self._currency = data['totales']['moneda'] + self._pagos = data.pop('pagos', False) self._comprobante(data['comprobante']) self._emisor(data['emisor']) self._receptor(data['receptor']) self._conceptos(data['conceptos']) + + if self._pagos: + self._cfdipays(data['pays']) + if 'nomina' in data and data['nomina']: self._nomina(data['nomina']) else: @@ -1167,6 +1271,7 @@ class LIBO(object): self._timbre(data['timbre']) self._donataria(data['donataria']) self._ine(data['ine']) + self._cancelado(data['cancelada']) self._clean() return @@ -1444,13 +1549,17 @@ def to_pdf(data, emisor_rfc, ods=False): if 'nomina' in data and data['nomina']: version = '{}_{}'.format(data['nomina']['version'], version) + pagos = '' + if data.get('pagos', False): + pagos = '_pagos_' + if APP_LIBO: app = LIBO() if app.is_running: donativo = '' if data['donativo']: donativo = '_donativo' - name = '{}_{}{}.ods'.format(rfc.lower(), version, donativo) + name = '{}_{}{}{}.ods'.format(rfc.lower(), pagos, version, donativo) path = get_template_ods(name) if path: return app.pdf(path, data, ods) @@ -1553,6 +1662,7 @@ def _comprobante(doc, options): 'I': 'ingreso', 'E': 'egreso', 'T': 'traslado', + 'P': 'pago', } data['tipodecomprobante'] = tipos.get(data['tipodecomprobante']) data['lugarexpedicion'] = \ @@ -1854,6 +1964,27 @@ def _nomina(doc, data, values, version_cfdi): return info +def _cfdipays(doc, data, version): + node = doc.find('{}Complemento/{}Pagos'.format(PRE[version], PRE['pagos'])) + if node is None: + return {} + + info = CaseInsensitiveDict(node.attrib.copy()) + related = [] + for n1 in node: + info.update(CaseInsensitiveDict(n1.attrib.copy())) + for n2 in n1: + related.append(CaseInsensitiveDict(n2.attrib.copy())) + + info['related'] = related + + data['comprobante']['totalenletras'] = to_letters( + float(info['monto']), info['monedap']) + data['comprobante']['moneda'] = info['monedap'] + + return info + + def get_data_from_xml(invoice, values): data = {'cancelada': invoice.cancelada, 'donativo': False} if hasattr(invoice, 'donativo'): @@ -1878,6 +2009,9 @@ def get_data_from_xml(invoice, values): data['comprobante'].update(data['timbre']) data['nomina'] = _nomina(doc, data, values, version) + data['pagos'] = values.get('pagos', False) + if data['pagos']: + data['pays'] = _cfdipays(doc, data, version) return data diff --git a/source/app/main.py b/source/app/main.py index b43ff83..7c21cc2 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -16,7 +16,8 @@ from controllers.main import (AppEmpresas, AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig, AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios, AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco, - AppMovimientosBanco, AppTickets, AppStudents, AppEmployees, AppNomina + AppMovimientosBanco, AppTickets, AppStudents, AppEmployees, AppNomina, + AppInvoicePay, AppCfdiPay ) @@ -55,6 +56,8 @@ api.add_route('/movbanco', AppMovimientosBanco(db)) api.add_route('/students', AppStudents(db)) api.add_route('/employees', AppEmployees(db)) api.add_route('/nomina', AppNomina(db)) +api.add_route('/invoicepay', AppInvoicePay(db)) +api.add_route('/cfdipay', AppCfdiPay(db)) # ~ Activa si usas waitress y NO estas usando servidor web diff --git a/source/app/models/db.py b/source/app/models/db.py index 41ff929..9faffbf 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -412,3 +412,13 @@ class StorageEngine(object): def importar_bdfl(self): return main.importar_bdfl() + + # ~ Revisado + def get_invoicepay(self, values): + return main.FacturasPagos.get_values(values) + + def get_cfdipay(self, values): + return main.CfdiPagos.get_values(values) + + def cfdipay(self, values): + return main.CfdiPagos.post(values) diff --git a/source/app/models/main.py b/source/app/models/main.py index b7112b8..7778e03 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # ~ Empresa Libre -# ~ Copyright (C) 2018 Mauricio Baeza Servin (web@correolibre.net) +# ~ Copyright (C) 2016-2018 Mauricio Baeza Servin (web@correolibre.net) # ~ # ~ This program is free software: you can redistribute it and/or modify # ~ it under the terms of the GNU General Public License as published by @@ -34,7 +34,8 @@ from controllers import util 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_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \ + DEFAULT_SERIE_CFDIPAY, DEFAULT_TYPE_CFDIPAY FORMAT = '{0:.2f}' @@ -213,6 +214,10 @@ def get_doc(type_doc, id, rfc): data, file_name = util.get_log('nomina') elif type_doc == 'nompdf': data, file_name = CfdiNomina.get_pdf(id, rfc) + elif type_doc == 'xmlpago': + data, file_name = CfdiPagos.get_file_xml(id) + elif type_doc == 'pdfpago': + data, file_name = CfdiPagos.get_file_pdf(id) return data, file_name, content_type @@ -891,10 +896,15 @@ class Certificado(BaseModel): rfc = TextField(default='') desde = DateTimeField(null=True) hasta = DateTimeField(null=True) + es_fiel = BooleanField(default=False) 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) @@ -1933,11 +1943,13 @@ class MovimientosBanco(BaseModel): .select( MovimientosBanco.id, MovimientosBanco.fecha, + SATFormaPago.name.alias('way_payment'), MovimientosBanco.numero_operacion, MovimientosBanco.descripcion, MovimientosBanco.retiro, MovimientosBanco.deposito, MovimientosBanco.saldo) + .join(SATFormaPago).switch(MovimientosBanco) .where(filtros) .dicts() ) @@ -1945,21 +1957,6 @@ class MovimientosBanco(BaseModel): return {'ok': True, 'rows': rows} -class CfdiPagos(BaseModel): - movimiento = ForeignKeyField(MovimientosBanco) - fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) - fecha_timbrado = DateTimeField(null=True) - xml = TextField(default='') - uuid = UUIDField(null=True) - estatus = TextField(default='Guardado') - estatus_sat = TextField(default='') - notas = TextField(default='') - cancelado = BooleanField(default=False) - - class Meta: - order_by = ('movimiento',) - - class SATUsoCfdi(BaseModel): key = TextField(index=True, unique=True) name = TextField(default='', index=True) @@ -5220,6 +5217,366 @@ class FacturasPagos(BaseModel): cls._actualizar_saldo_cliente(cls, fac.cliente, importe * -1) return + def _get_related(self, values): + id = int(values['id']) + filters = (FacturasPagos.movimiento==id) + + rows = tuple(FacturasPagos + .select( + Facturas.id, + Facturas.serie, + Facturas.folio, + Facturas.uuid, + Facturas.fecha, + Facturas.tipo_comprobante, + Facturas.estatus, + Socios.nombre.alias('cliente'), + Facturas.total, + FacturasPagos.saldo, + FacturasPagos.importe, + ) + .join(Facturas).switch(FacturasPagos) + .join(Socios, on=(Facturas.cliente==Socios.id)) + .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 CfdiPagos(BaseModel): + movimiento = ForeignKeyField(MovimientosBanco) + socio = ForeignKeyField(Socios) + serie = TextField(default='') + 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='') + uuid = UUIDField(null=True) + estatus = TextField(default='Guardada') + estatus_sat = TextField(default='') + notas = TextField(default='') + error = TextField(default='') + cancelada = BooleanField(default=False) + + class Meta: + order_by = ('movimiento',) + + @classmethod + def post(cls, values): + opt = values.pop('opt') + return getattr(cls, '_{}'.format(opt))(cls, values) + + def _get_folio(self, serie): + folio = int(Configuracion.get_('txt_config_cfdipay_folio') or '0') + start = (CfdiPagos + .select(fn.Max(CfdiPagos.folio).alias('mf')) + .where(CfdiPagos.serie==serie) + .order_by(SQL('mf')) + .scalar()) + + if start is None: + next_folio = 1 + else: + next_folio = start + 1 + + if folio > next_folio: + next_folio = folio + + return next_folio + + def _new(self, values): + id_mov = int(values['id_mov']) + + filters = (FacturasPagos.movimiento==id_mov) + related = FacturasPagos.select().where(filters) + if not related: + msg = 'El pago no tiene facturas relacionadas' + data = {'ok': False, 'msg': msg} + return data + + partner = tuple(set([f.factura.cliente.id for f in related])) + if len(partner) > 1: + msg = 'Facturas relacionadas a diferentes clientes' + data = {'ok': False, 'msg': msg} + return data + + partner = partner[0] + partner_name = related[0].factura.cliente.nombre + regimen_fiscal = related[0].factura.regimen_fiscal + + filters = ( + (CfdiPagos.movimiento==id_mov) & + (CfdiPagos.cancelado==False) + ) + previous = CfdiPagos.select().where(filters) + if previous: + 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 + fields = {} + fields['movimiento'] = id_mov + fields['socio'] = partner + 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) + + row = { + '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, 'new': True} + return data + + def _get_related_xml(self, id_mov, currency): + 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: + # ~ print('\n\nMONEDA', currency, r['MonedaDR']) + r['IdDocumento'] = str(r['IdDocumento']) + r['Folio'] = str(r['Folio']) + r['NumParcialidad'] = str(r['NumParcialidad']) + # ~ r['TipoCambioDR'] = FORMAT.format(r['TipoCambioDR']) + r['MetodoDePagoDR'] = 'PDD' + 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 + currency = mov.cuenta.moneda.key + related_docs = self._get_related_xml(self, invoice.movimiento, currency) + pagos = { + 'FechaPago': mov.fecha.isoformat()[:19], + 'FormaDePagoP': mov.forma_pago.key, + 'MonedaP': currency, + 'Monto': FORMAT.format(mov.deposito), + 'relacionados': related_docs, + } + + 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() + msg = 'Factura timbrada correctamente' + result = util.timbra_xml(obj.xml, auth) + if result['ok']: + obj.xml = result['xml'] + obj.uuid = result['uuid'] + obj.fecha_timbrado = result['fecha'] + obj.estatus = 'Timbrada' + obj.error = '' + row = {'uuid': obj.uuid, 'estatus': 'Timbrada'} + else: + msg = result['error'] + obj.estatus = 'Error' + obj.error = msg + row = {'estatus': 'Error'} + + obj.save() + + result = { + 'ok': result['ok'], + 'msg': msg, + 'id': obj.id, + 'row': row, + } + return result + + 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_file_xml(cls, id): + obj = CfdiPagos.get(CfdiPagos.id==id) + folio = str(obj.folio).zfill(6) + name = '{}{}_{}.xml'.format(obj.serie, folio, obj.socio.rfc) + return obj.xml, name + + def _get_not_in_xml(self, invoice, emisor): + values = {} + + values['notas'] = invoice.notas + values['fechadof'] = None + + obj = SATRegimenes.get(SATRegimenes.key==invoice.regimen_fiscal) + values['regimenfiscal'] = str(obj) + + obj = SATUsoCfdi.get(SATUsoCfdi.key=='P01') + values['usocfdi'] = str(obj) + + values['moneda'] = 'XXX' + + if invoice.tipo_relacion: + obj = SATTipoRelacion.get(SATTipoRelacion.key==invoice.tipo_relacion) + values['tiporelacion'] = str(obj) + + receptor = Socios.select().where(Socios.id==invoice.socio.id).dicts()[0] + values['receptor'] = {} + for k, v in receptor.items(): + values['receptor'][k] = v + + values['pagos'] = True + return values + + @classmethod + def get_file_pdf(cls, id): + try: + emisor = Emisor.select()[0] + except IndexError: + return b'', 'sin_datos_de_emisor.pdf' + + obj = CfdiPagos.get(CfdiPagos.id==id) + folio = str(obj.folio).zfill(6) + name = '{}{}_{}.pdf'.format(obj.serie, folio, obj.socio.rfc) + + if obj.uuid is None: + return b'', name + + values = cls._get_not_in_xml(cls, obj, emisor) + data = util.get_data_from_xml(obj, values) + obj = SATFormaPago.get(SATFormaPago.key==data['pays']['FormaDePagoP']) + data['pays']['formadepago'] = '{} ({})'.format(obj.name, obj.key) + doc = util.to_pdf(data, emisor.rfc) + + return doc, name + + @classmethod + def get_values(cls, values): + opt = values.pop('opt') + return getattr(cls, '_get_{}'.format(opt))(cls, values) + class PreFacturasImpuestos(BaseModel): factura = ForeignKeyField(PreFacturas) @@ -7487,6 +7844,35 @@ def _migrate_tables(): if 'cancelado' in columns: migrations.append(migrator.drop_column('facturaspagos', 'cancelado')) + columns = [c.name for c in database_proxy.get_columns('certificado')] + if not 'es_fiel' in columns: + es_fiel = BooleanField(default=False) + migrations.append(migrator.add_column('certificado', 'es_fiel', es_fiel)) + + columns = [c.name for c in database_proxy.get_columns('cfdipagos')] + if not 'serie' in columns: + socio = ForeignKeyField(Socios, null=True, to_field=Socios.id) + 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) + cancelada = BooleanField(default=False) + 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)) + migrations.append(migrator.add_column('cfdipagos', 'socio_id', socio)) + migrations.append(migrator.drop_column('cfdipagos', 'cancelado')) + migrations.append(migrator.add_column('cfdipagos', 'cancelada', cancelada)) + if migrations: with database_proxy.atomic() as txn: migrate(*migrations) diff --git a/source/app/settings.py b/source/app/settings.py index 8156b47..d94ec16 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -1,4 +1,20 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +# ~ Empresa Libre +# ~ Copyright (C) 2016-2018 Mauricio Baeza Servin (web@correolibre.net) +# ~ +# ~ This program is free software: you can redistribute it and/or modify +# ~ it under the terms of the GNU General Public License as published by +# ~ the Free Software Foundation, either version 3 of the License, or +# ~ (at your option) any later version. +# ~ +# ~ This program is distributed in the hope that it will be useful, +# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of +# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# ~ GNU General Public License for more details. +# ~ +# ~ You should have received a copy of the GNU General Public License +# ~ along with this program. If not, see . import logbook import os @@ -31,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) @@ -116,7 +132,8 @@ PRE = { 'NOMINA': { '1.1': '{http://www.sat.gob.mx/nomina}', '1.2': '{http://www.sat.gob.mx/nomina12}', - } + }, + 'pagos': '{http://www.sat.gob.mx/Pagos}', } CURRENT_CFDI = '3.3' @@ -138,6 +155,8 @@ 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 09e7656..05e1efa 100644 --- a/source/static/js/controller/bancos.js +++ b/source/static/js/controller/bancos.js @@ -1,3 +1,20 @@ +//~ Empresa Libre +//~ Copyright (C) 2016-2018 Mauricio Baeza Servin (web@correolibre.net) +//~ +//~ This program is free software: you can redistribute it and/or modify +//~ it under the terms of the GNU General Public License as published by +//~ the Free Software Foundation, either version 3 of the License, or +//~ (at your option) any later version. +//~ +//~ This program is distributed in the hope that it will be useful, +//~ but WITHOUT ANY WARRANTY; without even the implied warranty of +//~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//~ GNU General Public License for more details. +//~ +//~ You should have received a copy of the GNU General Public License +//~ along with this program. If not, see . + + var msg = '' var msg_importe = '' @@ -7,6 +24,7 @@ var bancos_controllers = { $$('lst_cuentas_banco').attachEvent('onChange', lst_cuentas_banco_change) $$('cmd_agregar_retiro').attachEvent('onItemClick', cmd_agregar_retiro_click) $$('cmd_agregar_deposito').attachEvent('onItemClick', cmd_agregar_deposito_click) + $$('cmd_complemento_pago').attachEvent('onItemClick', cmd_complemento_pago_click) $$('cmd_guardar_retiro').attachEvent('onItemClick', cmd_guardar_retiro_click) $$('cmd_guardar_deposito').attachEvent('onItemClick', cmd_guardar_deposito_click) $$('cmd_cancelar_movimiento').attachEvent('onItemClick', cmd_cancelar_movimiento_click) @@ -20,6 +38,11 @@ var bancos_controllers = { $$('filter_cuenta_year').attachEvent('onChange', filter_cuenta_change) $$('filter_cuenta_month').attachEvent('onChange', filter_cuenta_change) $$('filter_cuenta_dates').attachEvent('onChange', filter_cuenta_dates_change) + + $$('cmd_pay_stamp').attachEvent('onItemClick', cmd_pay_stamp_click) + $$('cmd_pay_cancel').attachEvent('onItemClick', cmd_pay_cancel_click) + $$('grid_cfdi_pay').attachEvent('onItemClick', grid_cfdi_pay_click) + set_year_month() } } @@ -666,8 +689,202 @@ function filter_cuenta_change(){ get_estado_cuenta() } + function filter_cuenta_dates_change(range){ if(range.start != null && range.end != null){ get_estado_cuenta(range) } -} \ No newline at end of file +} + + +function set_data_pay(row){ + var form = $$('form_bank_pay') + var dt = row.fecha.split(' ') + var grid = $$('grid_pay_related') + grid.clearAll() + + set_way_payment('pay_way_payment') + var wp = table_waypayment.findOne({'value': row.way_payment}) + + form.setValues({ + id_mov: row.id, + pay_date: dt[0], + pay_time: dt[1], + pay_reference: row.numero_operacion, + pay_way_payment: wp.id, + pay_import: row.deposito, + pay_description: row.descripcion + }) + + webix.ajax().get('/invoicepay', {'opt': 'related', 'id': 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.parse(values.rows, 'json') + } + } + }) + + $$('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') + } + } + }) + +} + + +function cmd_complemento_pago_click(){ + var grid = $$('grid_cuentabanco') + + var row = grid.getSelectedItem() + if(row == undefined){ + msg_error('Selecciona un movimiento de depósito') + return + } + if(row.descripcion == 'Saldo inicial'){ + msg_error('No es posible generar un pago del Saldo Inicial') + return + } + if(row.deposito == 0){ + msg_error('Selecciona un movimiento de depósito') + return + } + + set_data_pay(row) + $$('multi_bancos').setValue('bank_pay') +} + + +function validate_cfdi_pay(form){ + if(!form.validate()) { + msg_error('Valores inválidos') + 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 g = $$('grid_cfdi_pay') + 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){ + g.updateItem(result.id, result.row) + msg_ok(result.msg) + }else{ + msg_error(result.msg) + } + } + }) +} + +function save_cfdi_pay(form){ + var values = form.getValues() + var data = {'opt': 'new', 'id_mov': values.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){ + 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) + } + } + }) +} + + +function cmd_pay_stamp_click(){ + var form = $$('form_bank_pay') + var title = 'Timbrar Factura de Pago' + msg = '¿Estás seguro de enviar a timbrar este pago?' + + if (!validate_cfdi_pay(form)){ + return + } + + webix.confirm({ + title: title, + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + save_cfdi_pay(form) + } + } + }) +} + + +function cmd_pay_cancel_click(){ +} + + +function grid_cfdi_pay_click(id, e, node){ + var row = this.getItem(id) + + if(id.column == 'xml'){ + location = '/doc/xmlpago/' + row.id + }else if(id.column == 'pdf'){ + window.open('/doc/pdfpago/' + row.id, '_blank') + }else if(id.column == 'email'){ + //~ enviar_correo(row) + } + +} diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index 004011f..de96b83 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -39,6 +39,8 @@ function configuracion_inicial(){ add_config({'key': 'decimales_precios', 'value': values.decimales_precios}) }) + get_way_payment() + } diff --git a/source/static/js/controller/util.js b/source/static/js/controller/util.js index 7340349..822a985 100644 --- a/source/static/js/controller/util.js +++ b/source/static/js/controller/util.js @@ -1,3 +1,20 @@ +//~ Empresa Libre +//~ Copyright (C) 2016-2018 Mauricio Baeza Servin (web@correolibre.net) +//~ +//~ This program is free software: you can redistribute it and/or modify +//~ it under the terms of the GNU General Public License as published by +//~ the Free Software Foundation, either version 3 of the License, or +//~ (at your option) any later version. +//~ +//~ This program is distributed in the hope that it will be useful, +//~ but WITHOUT ANY WARRANTY; without even the implied warranty of +//~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//~ GNU General Public License for more details. +//~ +//~ You should have received a copy of the GNU General Public License +//~ along with this program. If not, see . + + var PUBLICO = "Público en general"; var RFC_PUBLICO = "XAXX010101000"; var RFC_EXTRANJERO = "XEXX010101000"; @@ -16,6 +33,7 @@ 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') +var table_waypayment = db.addCollection('waypayment') var msg = '' @@ -408,6 +426,21 @@ function get_forma_pago(control){ } +function get_way_payment(){ + webix.ajax().get('/values/formapago', {key: true}, function(text, data){ + var values = data.json() + table_waypayment.clear() + table_waypayment.insert(values) + }) +} + + +function set_way_payment(control){ + var values = table_waypayment.chain().data() + $$(control).getList().parse(values) +} + + function validate_regexp(value, pattern){ re = new RegExp(pattern, 'i'); if(value.match(re)){ @@ -443,3 +476,7 @@ function pause(milliseconds) { var dt = new Date(); while ((new Date()) - dt <= milliseconds) { /* Do nothing */ } } + + +//~ Revisado + diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 248f6d2..7936869 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -233,18 +233,38 @@ var emisor_otros_datos= [ ] -var emisor_certificado = [ +var col_sello = {rows: [ {template: 'Certificado actual', type: 'section'}, {view: 'form', id: 'form_cert', rows: [ {cols: [{view: 'text', id: 'cert_rfc', name: 'cert_rfc', - label: 'RFC: ', readonly: true, placeholder: 'Ninguno'}, {}]}, + label: 'RFC: ', readonly: true, placeholder: 'Ninguno'}]}, {cols: [{view: 'text', id: 'cert_serie', name: 'cert_serie', - label: 'Serie: ', readonly: true, placeholder: 'Ninguno'}, {}]}, + label: 'Serie: ', readonly: true, placeholder: 'Ninguno'}]}, {cols: [{view: 'text', id: 'cert_desde', name: 'cert_desde', - label: 'Vigente desde: ', readonly: true}, {}]}, + label: 'Vigente desde: ', readonly: true}]}, {cols: [{view: 'text', id: 'cert_hasta', name: 'cert_hasta', - label: 'Vigente hasta: ', readonly: true}, {}]}, - ]}, + label: 'Vigente hasta: ', readonly: true}]}, + ]} +]} + + +var col_fiel = {rows: [ + {template: 'Fiel actual', type: 'section'}, + {view: 'form', id: 'form_fiel', rows: [ + {cols: [{view: 'text', id: 'fiel_rfc', name: 'fiel_rfc', + label: 'RFC: ', readonly: true, placeholder: 'Ninguno'}]}, + {cols: [{view: 'text', id: 'fiel_serie', name: 'fiel_serie', + label: 'Serie: ', readonly: true, placeholder: 'Ninguno'}]}, + {cols: [{view: 'text', id: 'fiel_desde', name: 'fiel_desde', + label: 'Vigente desde: ', readonly: true}]}, + {cols: [{view: 'text', id: 'fiel_hasta', name: 'fiel_hasta', + label: 'Vigente hasta: ', readonly: true}]}, + ]} +]} + + +var emisor_certificado = [ + {cols: [col_sello, col_fiel]}, {template: 'Cargar Certificado', type: 'section'}, {view: 'form', id: 'form_upload', rows: [ {cols: [{}, diff --git a/source/static/js/ui/bancos.js b/source/static/js/ui/bancos.js index b97128b..5d161af 100644 --- a/source/static/js/ui/bancos.js +++ b/source/static/js/ui/bancos.js @@ -1,3 +1,18 @@ +//~ Empresa Libre +//~ Copyright (C) 2016-2018 Mauricio Baeza Servin (web@correolibre.net) +//~ +//~ This program is free software: you can redistribute it and/or modify +//~ it under the terms of the GNU General Public License as published by +//~ the Free Software Foundation, either version 3 of the License, or +//~ (at your option) any later version. +//~ +//~ This program is distributed in the hope that it will be useful, +//~ but WITHOUT ANY WARRANTY; without even the implied warranty of +//~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//~ GNU General Public License for more details. +//~ +//~ You should have received a copy of the GNU General Public License +//~ along with this program. If not, see . var toolbar_banco = [ @@ -26,6 +41,9 @@ 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: 'Factura de Pago', + type: 'iconButton', autowidth: true, icon: 'file-code-o'}, + {}, {view: 'button', id: 'cmd_cancelar_movimiento', label: 'Cancelar', type: 'iconButton', autowidth: true, icon: 'ban'}, ] @@ -35,6 +53,7 @@ var grid_cuentabanco_cols = [ {id: 'id', header:'ID', hidden: true}, {id: 'fecha', header: 'Fecha', width: 150}, {id: 'numero_operacion', header: 'Referencia'}, + {id: 'way_payment', header: 'Forma de Pago', hidden: true}, {id: 'descripcion', header: ['Descripción', {content: 'textFilter'}], fillspace:true}, {id: 'retiro', header: ['Retiro', {content: 'numberFilter'}], @@ -95,6 +114,49 @@ var grid_cfdi_este_deposito_cols = [ ] +var grid_cfdi_pay_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'}, + {id: 'folio', header: ['Folio'], adjust: 'data', + sort: 'int', css: 'right', footer: {text: 'Facturas', colspan: 3}}, + {id: "uuid", header: ["UUID"], adjust: "data", + sort:"string", hidden:true}, + {id: "fecha", header: ["Fecha y Hora"], + adjust: "data", sort: "string"}, + {id: "tipo_comprobante", header: ["Tipo"], + adjust: 'header', sort: 'string'}, + {id: "estatus", header: ["Estatus"], + adjust: "data", sort:"string"}, + {id: 'cliente', header: ['Razón Social'], fillspace: true}, + {id: 'xml', header: 'XML', adjust: 'data', template: get_icon('xml')}, + {id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')}, + {id: 'email', header: '', adjust: 'data', template: get_icon('email')} +] + + +var grid_pay_related_cols = [ + {id: 'index', header: '#', adjust: 'data', css: 'right'}, + {id: 'id', header: 'ID', hidden: true}, + {id: 'serie', header: 'Serie', adjust: 'data'}, + {id: 'folio', header: 'Folio', adjust: 'data', css: 'right'}, + {id: 'uuid', header: 'UUID', width: 250, hidden: true}, + {id: 'fecha', header: 'Fecha y Hora', width: 150, sort: 'date'}, + {id: 'tipo_comprobante', header: 'Tipo', adjust: 'data'}, + {id: 'estatus', header: 'Estatus', adjust: 'header'}, + {id: 'cliente', header: ['Razón Social'], fillspace: true}, + {id: 'total', header: ['Total'], width: 125, sort: 'int', + format: webix.i18n.priceFormat, css: 'right'}, + {id: 'saldo', header: ['Saldo'], width: 125, sort: 'int', + format: webix.i18n.priceFormat, css: 'right', css: 'right', + footer: 'Importe Depósito'}, + {id: 'importe', header: ['Este pago'], width: 125, sort: 'int', + format: webix.i18n.priceFormat, css: 'right', + footer: {content: 'summColumn', css: 'right'}}, +] + + var grid_cfdi_por_pagar = { view: 'datatable', id: 'grid_cfdi_por_pagar', @@ -139,6 +201,47 @@ var grid_cfdi_este_deposito = { } +var grid_cfdi_pay = { + view: 'datatable', + id: 'grid_cfdi_pay', + select: 'row', + autoConfig: false, + adjust: true, + autoheight: true, + resizeColumn: true, + headermenu: true, + columns: grid_cfdi_pay_cols, + on:{ + 'data->onStoreUpdated':function(){ + this.data.each(function(obj, i){ + obj.index = i + 1 + }) + } + }, +} + + +var grid_pay_related = { + view: 'datatable', + id: 'grid_pay_related', + select: 'row', + autoConfig: false, + adjust: true, + autoheight: true, + resizeColumn: true, + headermenu: true, + footer: true, + columns: grid_pay_related_cols, + on:{ + 'data->onStoreUpdated':function(){ + this.data.each(function(obj, i){ + obj.index = i + 1 + }) + } + }, +} + + var toolbar_banco_retiro = [ {view: 'label', label: 'Agregar retiro de banco'}, {}, @@ -159,6 +262,19 @@ var toolbar_banco_deposito = [ ] +var toolbar_bank_pay = [ + {view: 'label', label: 'Factura de pago'}, + {}, + {view: 'button', id: 'cmd_pay_stamp', label: 'Timbrar', + type: 'iconButton', autowidth: true, icon: 'ticket'}, + {view: 'button', id: 'cmd_pay_cancel', label: 'Cancelar', + type: 'iconButton', autowidth: true, icon: 'ban'}, + {}, + {view: 'icon', click: '$$("multi_bancos").setValue("banco_home")', + icon: 'times-circle'} +] + + var controls_banco_retiro = [ {view: 'toolbar', elements: toolbar_banco_retiro}, {cols: [ @@ -229,6 +345,41 @@ var controls_banco_deposito = [ ] +var controls_bank_pay = [ + {view: 'toolbar', elements: toolbar_bank_pay}, + {view: 'label', label: 'Este depósito: '}, + {cols: [ + {view: 'datepicker', id: 'pay_date', name: 'pay_date', + label: 'Fecha', format: '%d-%M-%Y', labelAlign: 'right', + required: true, invalidMessage: 'Selecciona una fecha', + labelWidth: 125, readonly: true}, + {view: 'search', id: 'pay_time', name: 'pay_time', label: 'Hora', + icon: 'clock-o', labelAlign: 'right', required: true, + readonly: true, + invalidMessage: 'Captura una hora'}, + {view: 'text', id: 'pay_reference', name: 'pay_reference', + label: 'Referencia', labelAlign: 'right', readonly: true}, + ]}, + {cols: [ + {view: 'richselect', id: 'pay_way_payment', readonly: true, + name: 'pay_way_payment', label: 'Forma de Pago', required: true, + options: [], labelWidth: 125, labelAlign: 'right'}, + {view: 'currency', type: 'text', id: 'pay_import', name: 'pay_import', + label: 'Importe', labelAlign: 'right', required: true, readonly: true, + invalidMessage: 'Captura un valor númerico', inputAlign: 'right'} + ]}, + {cols: [ + {view: 'textarea', id: 'pay_description', label: 'Descripción', + name: 'pay_description', labelAlign: 'right', required: true, + labelWidth: 125, height: 70, readonly: true}, + ]}, + {view: 'label', label: 'Facturas de pago de este depósito: '}, + grid_cfdi_pay, + {view: 'label', label: 'Facturas relacionadas en este pago: '}, + grid_pay_related +] + + var form_banco_retiro = { type: 'space', responsive: true, @@ -255,6 +406,19 @@ var form_banco_deposito = { } +var form_bank_pay = { + type: 'space', + responsive: true, + cols: [{ + view: 'form', + id: 'form_bank_pay', + complexData: true, + scroll: true, + elements: controls_bank_pay, + }], +} + + var multi_bancos = { id: 'multi_bancos', animate: true, @@ -266,7 +430,8 @@ var multi_bancos = { grid_cuentabanco, ]}, {id: 'banco_retiro', rows: [form_banco_retiro]}, - {id: 'banco_deposito', rows: [form_banco_deposito]} + {id: 'banco_deposito', rows: [form_banco_deposito]}, + {id: 'bank_pay', rows: [form_bank_pay]} ], } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index af3e8e8..516bdc2 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -1,5 +1,5 @@ //~ Empresa Libre -//~ Copyright (C) 2018 Mauricio Baeza Servin (web@correolibre.net) +//~ Copyright (C) 2016-2018 Mauricio Baeza Servin (web@correolibre.net) //~ //~ This program is free software: you can redistribute it and/or modify //~ it under the terms of the GNU General Public License as published by diff --git a/source/templates/plantilla_pagos.ods b/source/templates/plantilla_pagos.ods new file mode 100644 index 0000000..975e9a7 Binary files /dev/null and b/source/templates/plantilla_pagos.ods differ diff --git a/source/xslt/cadena.xslt b/source/xslt/cadena.xslt index 8ed4715..81d45ac 100644 --- a/source/xslt/cadena.xslt +++ b/source/xslt/cadena.xslt @@ -12,6 +12,7 @@ + diff --git a/source/xslt/pagos10.xslt b/source/xslt/pagos10.xslt new file mode 100644 index 0000000..98b41f2 --- /dev/null +++ b/source/xslt/pagos10.xslt @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +