Merge branch 'develop'

Cancelar factura de pago
This commit is contained in:
Mauricio Baeza 2018-09-10 00:18:39 -05:00
commit 9081d54865
12 changed files with 279 additions and 40 deletions

View File

@ -1,3 +1,19 @@
v 1.13.0 [10-sep-2018]
----------------------
- Cancelar factura 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 -bk
python main.py -m
```
v 1.12.0 [31-ago-2018]
----------------------
- Soporte para facturas (complemento) de pago.

View File

@ -1 +1 @@
1.12.0
1.13.0

View File

@ -420,7 +420,8 @@ class AppMovimientosBanco(object):
def on_post(self, req, resp):
values = req.params
req.context['result'] = self._db.add_movbanco(values)
# ~ req.context['result'] = self._db.add_movbanco(values)
req.context['result'] = self._db.bankmovement(values)
resp.status = falcon.HTTP_200
def on_delete(self, req, resp):

View File

@ -2509,6 +2509,26 @@ def local_copy(files):
return
def sync_files(files, auth={}):
if not MV:
return
path_bk = _join(str(Path.home()), DIR_FACTURAS)
if not os.path.isdir(path_bk):
msg = 'No existe la carpeta: facturas'
log.error(msg)
return
for obj, name, target in files:
path = _validar_directorios(path_bk, target)
path_file = _join(path, name)
m = 'wb'
if name.endswith('xml'):
m = 'w'
save_file(path_file, obj, m)
return
def sync_cfdi(auth, files):
local_copy(files)

View File

@ -389,8 +389,8 @@ class StorageEngine(object):
def cuentasbanco(self, values):
return main.CuentasBanco.add(values)
def add_movbanco(self, values):
return main.MovimientosBanco.add(values)
# ~ def add_movbanco(self, values):
# ~ return main.MovimientosBanco.add(values)
def get_cuentasbanco(self, values):
return main.CuentasBanco.get_(values)
@ -422,3 +422,6 @@ class StorageEngine(object):
def cfdipay(self, values):
return main.CfdiPagos.post(values)
def bankmovement(self, values):
return main.MovimientosBanco.post(values)

View File

@ -1856,8 +1856,18 @@ class MovimientosBanco(BaseModel):
return saldo
@classmethod
def add(cls, values):
def post(cls, values):
opt = values.pop('opt')
return getattr(cls, '_{}'.format(opt))(cls, values)
def _add(self, values):
ids = values.pop('ids', '')
if ids and not Facturas.validate_count_partners(util.loads(ids)):
msg = 'Facturas relacionadas a diferentes clientes'
data = {'ok': False, 'msg': msg}
return data
actualizar = False
if 'saldo' in values:
saldo = values['saldo']
@ -1870,8 +1880,8 @@ class MovimientosBanco(BaseModel):
values['deposito'] = util.get_float(values['deposito'])
values['forma_pago'] = int(values['forma_pago'])
ultimo_saldo = cls._ultimo_saldo(
cls, values['cuenta'], values['fecha'])
ultimo_saldo = self._ultimo_saldo(
self, values['cuenta'], values['fecha'])
values['saldo'] = \
ultimo_saldo - values['retiro'] + values['deposito']
@ -1883,31 +1893,66 @@ class MovimientosBanco(BaseModel):
return {'ok': False, 'msg': msg}
if actualizar:
saldo = cls._actualizar_saldos(cls, obj)
saldo = self._actualizar_saldos(self, obj)
if ids:
FacturasPagos.add(obj, util.loads(ids))
return {'ok': True, 'saldo': saldo}
@classmethod
def remove(cls, id):
def _cancel(self, values):
id = int(values['id'])
try:
obj = MovimientosBanco.get(MovimientosBanco.id==id)
except MovimientosBanco.DoesNotExist:
return False
msg = 'No se encontró el movimiento'
return {'ok': False, 'msg': msg}
if obj.conciliado or obj.cancelado:
return False
if obj.cancelado:
msg = 'El movimiento ya esta cancelado'
return {'ok': False, 'msg': msg}
if obj.conciliado:
msg = 'El movimiento esta conciliado, no se puede cancelar'
return {'ok': False, 'msg': msg}
filters = (CfdiPagos.movimiento==obj)
cp = CfdiPagos.select().where(filters).count()
if cp > 0:
msg = 'El movimiento tiene Factura de Pago, no se puede cancelar'
return {'ok': False, 'msg': msg}
with database_proxy.transaction():
obj.cancelado = True
obj.save()
FacturasPagos.cancelar(obj)
obj = cls._movimiento_anterior(cls, obj.cuenta, obj.fecha)
cls._actualizar_saldos(cls, obj)
obj = self._movimiento_anterior(self, obj.cuenta, obj.fecha)
self._actualizar_saldos(self, obj)
return True
balance = CuentasBanco.get_saldo(obj.cuenta.id)
msg = 'Movimiento cancelado correctamente'
return {'ok': True, 'msg': msg, 'balance': balance}
# ~ @classmethod
# ~ def remove(cls, id):
# ~ try:
# ~ obj = MovimientosBanco.get(MovimientosBanco.id==id)
# ~ except MovimientosBanco.DoesNotExist:
# ~ return False
# ~ if obj.conciliado or obj.cancelado:
# ~ return False
# ~ with database_proxy.transaction():
# ~ obj.cancelado = True
# ~ obj.save()
# ~ FacturasPagos.cancelar(obj)
# ~ obj = cls._movimiento_anterior(cls, obj.cuenta, obj.fecha)
# ~ cls._actualizar_saldos(cls, obj)
# ~ return True
@classmethod
def con(cls, id):
@ -3220,6 +3265,17 @@ class Facturas(BaseModel):
class Meta:
order_by = ('fecha',)
@classmethod
def validate_count_partners(cls, ids):
filters = (Facturas.id.in_(tuple(ids.keys())))
partners = (Facturas.select(fn.COUNT(Facturas.cliente))
.where(filters)
.group_by(Facturas.cliente)
.order_by(Facturas.cliente).tuples())
if len(partners) > 1:
return False
return True
@classmethod
def cancel(cls, id):
obj = Facturas.get(Facturas.id==id)
@ -5279,6 +5335,7 @@ class CfdiPagos(BaseModel):
notas = TextField(default='')
error = TextField(default='')
cancelada = BooleanField(default=False)
fecha_cancelacion = DateTimeField(null=True)
class Meta:
order_by = ('movimiento',)
@ -5288,6 +5345,46 @@ class CfdiPagos(BaseModel):
opt = values.pop('opt')
return getattr(cls, '_{}'.format(opt))(cls, values)
def _cancel(self, values):
id_mov = int(values['id_mov'])
filters = (
(CfdiPagos.movimiento==id_mov) &
(CfdiPagos.cancelada==False)
)
last = CfdiPagos.select().where(filters)
if not last:
msg = 'El depósito no tiene facturas de pago activas'
data = {'ok': False, 'msg': msg}
return data
if len(last) > 1:
msg = 'Hay más de una factura activa para este depósito'
data = {'ok': False, 'msg': msg}
return data
last = last[0]
if not last.uuid:
msg = 'La factura no esta timbrada'
data = {'ok': False, 'msg': msg}
return data
auth = Emisor.get_auth()
cert = Certificado.get_cert()
data, result = util.cancel_xml(auth, last.uuid, cert)
if data['ok']:
last.estatus = 'Cancelada'
last.error = ''
last.cancelada = True
last.fecha_cancelacion = result['Fecha']
msg = 'Factura cancelada correctamente'
else:
last.error = msg = data['msg']
last.save()
return {'ok': data['ok'], 'msg': msg, 'id': last.id}
def _get_folio(self, serie):
folio = int(Configuracion.get_('txt_config_cfdipay_folio') or '0')
start = (CfdiPagos
@ -5341,9 +5438,19 @@ class CfdiPagos(BaseModel):
data = {'ok': True, 'new': False}
return data
fields = {}
filters = (
(CfdiPagos.movimiento==id_mov) &
(CfdiPagos.cancelada==True)
)
previous = CfdiPagos.select().where(filters).order_by(CfdiPagos.id.desc())
if previous:
previous = previous[0]
fields['tipo_relacion'] = DEFAULT_CFDIPAY['TYPE_RELATION']
fields['uuid_relacionado'] = previous.uuid
emisor = Emisor.select()[0]
serie = Configuracion.get_('txt_config_cfdipay_serie') or DEFAULT_CFDIPAY['SERIE']
fields = {}
fields['movimiento'] = id_mov
fields['socio'] = partner
fields['serie'] = serie
@ -5417,7 +5524,7 @@ class CfdiPagos(BaseModel):
if invoice.tipo_relacion:
related = {
'tipo': invoice.tipo_relacion,
'cfdis': (invoice.uuid_relacionado,),
'cfdis': (str(invoice.uuid_relacionado),),
}
emisor = {
@ -5508,6 +5615,8 @@ class CfdiPagos(BaseModel):
'id': obj.id,
'row': row,
}
if result['ok']:
self._sync(self, obj.id)
return result
def _get_related(self, values):
@ -5533,6 +5642,14 @@ class CfdiPagos(BaseModel):
obj = CfdiPagos.get(CfdiPagos.id==id)
folio = str(obj.folio).zfill(6)
name = '{}{}_{}.xml'.format(obj.serie, folio, obj.socio.rfc)
emisor = Emisor.select()[0]
target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
files = (
(obj.xml, name, target),
)
cls._sync_files(cls, files)
return obj.xml, name
def _get_not_in_xml(self, invoice, emisor):
@ -5575,14 +5692,31 @@ class CfdiPagos(BaseModel):
if obj.uuid is None:
return b'', name
target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
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)
files = (
(doc, name, target),
)
cls._sync_files(cls, files)
return doc, name
@util.run_in_thread
def _sync(self, id):
CfdiPagos.get_file_xml(id)
CfdiPagos.get_file_pdf(id)
return
@util.run_in_thread
def _sync_files(self, files):
util.sync_files(files)
return
@classmethod
def get_values(cls, values):
opt = values.pop('opt')
@ -7821,14 +7955,17 @@ def _crear_tablas(rfc):
return True
def _migrate_tables():
def _migrate_tables(rfc=''):
from playhouse.migrate import PostgresqlMigrator, migrate
rfc = input('Introduce el RFC: ').strip().upper()
if not rfc:
msg = 'El RFC es requerido'
log.error(msg)
return
if rfc:
rfc = rfc.strip().upper()
else:
rfc = input('Introduce el RFC: ').strip().upper()
if not rfc:
msg = 'El RFC es requerido'
log.error(msg)
return
args = util.get_con(rfc)
if not args:
@ -7931,6 +8068,10 @@ def _migrate_tables():
migrations.append(migrator.drop_column('cfdipagos', 'cancelado'))
migrations.append(migrator.add_column('cfdipagos', 'cancelada', cancelada))
if not 'fecha_cancelacion' in columns:
fecha_cancelacion = DateTimeField(null=True)
migrations.append(migrator.add_column('cfdipagos', 'fecha_cancelacion', fecha_cancelacion))
if migrations:
with database_proxy.atomic() as txn:
migrate(*migrations)
@ -8840,7 +8981,7 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña,
sys.exit(0)
if opt['migrar_bd']:
_migrate_tables()
_migrate_tables(rfc)
sys.exit(0)
if opt['nuevo_superusuario']:

View File

@ -47,7 +47,7 @@ except ImportError:
DEBUG = DEBUG
VERSION = '1.12.0'
VERSION = '1.13.0'
EMAIL_SUPPORT = ('soporte@empresalibre.net',)
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
@ -164,6 +164,7 @@ DEFAULT_CFDIPAY = {
'KEYSAT': '84111506',
'UNITKEY': 'ACT',
'DESCRIPTION': 'Pago',
'TYPE_RELATION': '04',
}
DIR_FACTURAS = 'facturas'
USAR_TOKEN = False

View File

@ -47,6 +47,13 @@
font-size: 125%;
}
.link_forum {
font-weight: bold;
color: #610B0B;
text-decoration: none;
}
.link_forum:hover {text-decoration:underline;}
.delete {
text-align: center;
font-weight: bold;

View File

@ -282,6 +282,7 @@ function guardar_retiro(values){
var importe = get_float(values.retiro_importe)
var data = new Object()
data['opt'] = 'add'
data['cuenta'] = $$('lst_cuentas_banco').getValue()
data['fecha'] = values.retiro_fecha
data['hora'] = $$('time_retiro').getText()
@ -519,6 +520,7 @@ function guardar_deposito(values){
var grid = $$('grid_cfdi_este_deposito')
var data = new Object()
data['opt'] = 'add'
data['cuenta'] = $$('lst_cuentas_banco').getValue()
data['fecha'] = values.deposito_fecha
data['hora'] = $$('time_deposito').getText()
@ -610,17 +612,24 @@ function cmd_guardar_deposito_click(){
function cancelar_movimiento(id){
var grid = $$('grid_cuentabanco')
webix.ajax().del('/movbanco', {id: id}, function(text, xml, xhr){
if(xhr.status == 200){
get_estado_cuenta()
get_saldo_cuenta()
msg_ok('Movimiento cancelado correctamente')
}else{
msg_error('No se pudo eliminar')
var data = {'opt': 'cancel', 'id': id}
webix.ajax().post('/movbanco', data, {
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){
get_estado_cuenta()
$$('txt_cuenta_saldo').setValue(values.balance)
msg_ok(values.msg)
}else{
msg_error(values.msg)
}
}
})
}
@ -871,7 +880,7 @@ function save_cfdi_pay(form){
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?'
msg = '¿Estás seguro de enviar a timbrar este pago?<BR><BR>EL MOVIMIENTO YA NO PODRÁ SER MODIFICADO'
if (!validate_cfdi_pay(form)){
return
@ -893,6 +902,43 @@ function cmd_pay_stamp_click(){
function cmd_pay_cancel_click(){
var form = $$('form_bank_pay')
var values = form.getValues()
var data = {'opt': 'cancel', 'id_mov': values.id_mov}
var grid = $$('grid_cfdi_pay')
if(grid.count() == 0){
msg_error('El depósito no tiene facturas de pago activas')
return
}
msg = '¿Estás seguro de cancelar esta factura?\n\nESTA ACCIÓN NO SE PUEDE DESHACER'
webix.confirm({
title: 'Cancelar Factura',
ok: 'Si',
cancel: 'No',
type: 'confirm-error',
text: msg,
callback:function(result){
if(result){
webix.ajax().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){
values = data.json();
if(values.ok){
grid.updateItem(values.id, {'estatus': 'Cancelada'})
msg_ok(values.msg)
}else{
msg_error(values.msg)
}
}
})
}
}
})
}

View File

@ -336,8 +336,9 @@ var controls_banco_deposito = [
]},
{cols: [
{view: 'label', label: '<b>Facturas por pagar: </b>'},
{view: 'button', id: 'cmd_invoice_payed', label: 'Pagada directamente',
type: 'iconButton', autowidth: true, icon: 'check-circle'},
{view: 'button', id: 'cmd_invoice_payed', label: 'Solo marcar pagada',
type: 'iconButton', autowidth: true, icon: 'check-circle',
tooltip: 'No afecta a saldos'},
]},
grid_cfdi_por_pagar,
{view: 'label', label: '<b>Facturas a pagar en este depósito: </b>'},

View File

@ -59,6 +59,9 @@ var menu_user = {
}
var link_forum = "<a class='link_forum' target='_blank' href='https://gitlab.com/mauriciobaeza/empresa-libre/issues'>Foro de Soporte</a>";
var ui_main = {
rows: [
{view: 'toolbar', padding: 3, elements: [
@ -68,7 +71,7 @@ var ui_main = {
}
},
{view: 'label', id: 'lbl_title_main', label: '<b>Empresa Libre</b>'},
{},
{view: 'label', id: 'lbl_forum', label: link_forum, align: 'right'},
menu_user,
{view: 'button', id: 'cmd_update_timbres', type: 'icon', width: 45,
css: 'app_button', icon: 'bell-o', badge: 0},

View File

@ -20,7 +20,7 @@
<body>
<script type="text/javascript" charset="utf-8">
webix.debug = true;
webix.debug = false;
webix.i18n.setLocale("es-MX");
</script>