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",