Generar ticket

This commit is contained in:
Mauricio Baeza 2017-12-20 01:15:48 -06:00
parent ccdfacb476
commit 9b4d0bffd7
12 changed files with 505 additions and 36 deletions

View File

@ -269,6 +269,22 @@ class AppPreInvoices(object):
resp.status = falcon.HTTP_204
class AppTickets(object):
def __init__(self, db):
self._db = db
def on_get(self, req, resp):
values = req.params
req.context['result'] = self._db.get_tickets(values)
resp.status = falcon.HTTP_200
def on_post(self, req, resp):
values = req.params
req.context['result'] = self._db.tickets(values)
resp.status = falcon.HTTP_200
class AppEmisor(object):
def __init__(self, db):

View File

@ -16,7 +16,7 @@ from controllers.main import (AppEmpresas,
AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig,
AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios,
AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco,
AppMovimientosBanco
AppMovimientosBanco, AppTickets
)
@ -46,6 +46,7 @@ api.add_route('/partners', AppPartners(db))
api.add_route('/products', AppProducts(db))
api.add_route('/invoices', AppInvoices(db))
api.add_route('/preinvoices', AppPreInvoices(db))
api.add_route('/tickets', AppTickets(db))
api.add_route('/cuentasbanco', AppCuentasBanco(db))
api.add_route('/movbanco', AppMovimientosBanco(db))

View File

@ -221,6 +221,9 @@ class StorageEngine(object):
def _get_product(self, values):
return main.Productos.get_by(values)
def _get_productokey(self, values):
return main.Productos.get_by_key(values)
def get_partners(self, values):
return main.Socios.get_(values)
@ -251,6 +254,11 @@ class StorageEngine(object):
#~ return main.PreFacturas.actualizar(values, id)
return main.PreFacturas.add(values)
def tickets(self, values):
opt = values.pop('opt')
if opt == 'add':
return main.Tickets.add(values)
def get_invoices(self, values):
return main.Facturas.get_(values)

View File

@ -1977,6 +1977,34 @@ class Productos(BaseModel):
value += 1
return {'value': value}
@classmethod
def get_by_key(cls, values):
clave = values.get('key', '')
row = (Productos
.select(
Productos.id,
Productos.clave,
Productos.clave_sat,
Productos.descripcion,
SATUnidades.name.alias('unidad'),
Productos.valor_unitario,
Productos.descuento)
.join(SATUnidades).switch(Productos)
.where((Productos.clave==clave) | (Productos.codigo_barras==clave))
.dicts()
)
if len(row):
id = row[0]['id']
model_pt = Productos.impuestos.get_through_model()
taxes = tuple(model_pt
.select(
model_pt.productos_id.alias('product'),
model_pt.satimpuestos_id.alias('tax'))
.where(model_pt.productos_id==id).dicts())
return {'ok': True, 'row': row[0], 'taxes': taxes}
return {'ok': False}
@classmethod
def get_by(cls, values):
clave = values.get('id', '')
@ -3838,6 +3866,112 @@ class Tickets(BaseModel):
class Meta:
order_by = ('fecha',)
def _get_folio(self, serie):
inicio = (Tickets
.select(fn.Max(Tickets.folio).alias('mf'))
.where(Tickets.serie==serie)
.order_by(SQL('mf'))
.scalar())
if inicio is None:
inicio = 1
else:
inicio += 1
return inicio
def _calcular_totales(self, ticket, productos):
subtotal = 0
descuento_cfdi = 0
totals_tax = {}
total_trasladados = None
for producto in productos:
id_producto = producto.pop('id')
p = Productos.get(Productos.id==id_producto)
producto['descripcion'] = p.descripcion
producto['ticket'] = ticket.id
producto['producto'] = id_producto
cantidad = float(producto['cantidad'])
valor_unitario = float(p.valor_unitario)
descuento = float(producto['descuento'])
precio_final = valor_unitario - descuento
importe = round(cantidad * precio_final, DECIMALES)
producto['cantidad'] = cantidad
producto['valor_unitario'] = valor_unitario
producto['descuento'] = descuento
producto['precio_final'] = precio_final
producto['importe'] = importe
descuento_cfdi += descuento
subtotal += importe
TicketsDetalle.create(**producto)
base = producto['importe']
for tax in p.impuestos:
impuesto_producto = round(float(tax.tasa) * base, DECIMALES)
if tax.tipo == 'T' and tax.key != '000':
total_trasladados = (total_trasladados or 0) + impuesto_producto
if tax.id in totals_tax:
totals_tax[tax.id].base += base
totals_tax[tax.id].importe += impuesto_producto
else:
tax.base = base
tax.importe = impuesto_producto
totals_tax[tax.id] = tax
for tax in totals_tax.values():
if tax.tipo == 'E':
continue
ticket_tax = {
'ticket': ticket.id,
'impuesto': tax.id,
'base': tax.base,
'importe': tax.importe,
}
TicketsImpuestos.create(**ticket_tax)
total = subtotal + (total_trasladados or 0)
data = {
'subtotal': subtotal + descuento,
'descuento': descuento_cfdi,
'total': total,
'total_trasladados': total_trasladados,
}
return data
@classmethod
def add(cls, values):
productos = util.loads(values.pop('productos'))
values['serie'] = 'T'
values['folio'] = cls._get_folio(cls, values['serie'])
with database_proxy.atomic() as txn:
obj = Tickets.create(**values)
totals = cls._calcular_totales(cls, obj, productos)
obj.subtotal = totals['subtotal']
obj.descuento = totals['descuento']
obj.total_trasladados = totals['total_trasladados']
obj.total = totals['total']
obj.save()
row = {
'id': obj.id,
'folio': obj.folio,
'fecha': obj.fecha,
'estatus': obj.estatus,
'total': obj.total,
}
data = {'ok': True, 'row': row}
return data
class TicketsDetalle(BaseModel):
ticket = ForeignKeyField(Tickets)

View File

@ -24,6 +24,23 @@
font-size: 125%;
color: DarkRed;
}
.right_footer2 {
text-align: right;
font-weight: bold;
font-size: 150%;
color: DarkRed;
}
.right_footer3 {
text-align: right;
font-weight: bold;
font-size: 120%;
color: DarkBlue;
}
.footer3 {
font-weight: bold;
font-size: 120%;
color: DarkBlue;
}
.lbl_partner {
font-weight: bold;

View File

@ -41,14 +41,6 @@ function get_series(){
}
function get_forma_pago(){
webix.ajax().get('/values/formapago', {key: true}, function(text, data){
var values = data.json()
$$('lst_forma_pago').getList().parse(values)
})
}
function get_monedas(){
webix.ajax().get('/values/monedas', function(text, data){
var values = data.json()
@ -105,7 +97,7 @@ function default_config(){
table_taxes.insert(values)
})
get_series()
get_forma_pago()
get_forma_pago('lst_forma_pago')
get_monedas()
get_uso_cfdi()
get_regimen_fiscal()

View File

@ -19,8 +19,6 @@ function configuracion_inicial(){
}
})
}

View File

@ -7,20 +7,260 @@ var tickets_controllers = {
$$('cmd_nuevo_ticket').attachEvent('onItemClick', cmd_nuevo_ticket_click)
$$('cmd_generar_ticket').attachEvent('onItemClick', cmd_generar_ticket_click)
$$('cmd_cerrar_ticket').attachEvent('onItemClick', cmd_cerrar_ticket_click)
$$('tsearch_product_key').attachEvent('onKeyPress', tsearch_product_key_press)
$$('grid_tdetails').attachEvent('onItemClick', grid_ticket_details_click)
$$('grid_tdetails').attachEvent('onBeforeEditStop', grid_tickets_details_before_edit_stop)
$$('gt_productos_found').attachEvent('onValueSuggest', gt_productos_found_click)
}
}
function configuracion_inicial_ticket(){
webix.ajax().sync().get('/values/taxes', function(text, data){
var values = data.json()
table_taxes.clear()
table_taxes.insert(values)
})
get_forma_pago('lst_ticket_forma_pago')
table_pt.clear()
table_totals.clear()
}
function cmd_nuevo_ticket_click(){
configuracion_inicial_ticket()
$$('multi_tickets').setValue('tickets_new')
}
function validar_ticket(){
var grid = $$('grid_tdetails')
if(!grid.count()){
webix.UIManager.setFocus('tsearch_product_key')
msg = 'Agrega al menos un producto'
msg_error(msg)
return false
}
return true
}
function guardar_ticket(values){
var grid = $$('grid_tdetails')
//~ showvar(values)
var rows = grid.data.getRange()
for (i = 0; i < rows.length; i++) {
delete rows[i]['delete']
delete rows[i]['clave']
delete rows[i]['clave_sat']
delete rows[i]['unidad']
delete rows[i]['importe']
rows[i]['valor_unitario'] = parseFloat(rows[i]['valor_unitario'])
rows[i]['descuento'] = parseFloat(rows[i]['descuento'])
}
var data = new Object()
data['opt'] = 'add'
data['productos'] = rows
data['forma_pago'] = values.forma_pago
webix.ajax().sync().post('tickets', 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){
msg_ok('Ticket generado correctamente')
$$('form_new_ticket').setValues({})
$$('multi_tickets').setValue('tickets_home')
}else{
msg_error(values.msg)
}
}
})
}
function cmd_generar_ticket_click(){
showvar('ok')
var form = this.getFormView();
if(!form.validate()) {
msg_error('Valores inválidos')
return
}
var values = form.getValues()
if(!validar_ticket()){
return
}
msg = '¿Todos los datos son correctos?<BR><BR>'
msg += '¿Estás seguro de generar este Ticket?'
webix.confirm({
title: 'Generar Ticket',
ok: 'Si',
cancel: 'No',
type: 'confirm-error',
text: msg,
callback:function(result){
if(result){
guardar_ticket(values)
}
}
})
}
function cmd_cerrar_ticket_click(){
$$('multi_tickets').setValue('tickets_home')
}
}
function calcular_precio_con_impuestos(precio, taxes){
var precio_final = precio
for(var tax of taxes){
impuesto = table_taxes.findOne({'id': tax.tax})
if(impuesto.tipo == 'E'){
continue
}
var base = precio
if(impuesto.tipo == 'R'){
base = (precio * -1).round(DECIMALES)
}
precio_final += (impuesto.tasa * base).round(DECIMALES)
}
return precio_final
}
function agregar_producto(values){
var taxes = values.taxes
var producto = values.row
var form = $$('form_new_ticket')
var grid = $$('grid_tdetails')
var row = grid.getItem(producto.id)
var precio_final = 0.0
if(row == undefined){
producto['cantidad'] = 1
producto['valor_unitario'] = calcular_precio_con_impuestos(
parseFloat(producto['valor_unitario']), taxes)
producto['importe'] = producto['valor_unitario']
grid.add(producto)
}else{
producto['cantidad'] = parseFloat(row.cantidad) + 1
producto['descuento'] = parseFloat(row.descuento)
producto['valor_unitario'] = parseFloat(row.valor_unitario)
precio_final = producto['valor_unitario'] - producto['descuento']
producto['importe'] = (precio_final * producto['cantidad']).round(DECIMALES)
grid.updateItem(row.id, producto)
}
form.setValues({tsearch_product_key: '', tsearch_product_name: ''}, true)
}
function buscar_producto_key(key){
webix.ajax().get('/values/productokey', {'key': key}, {
error: function(text, data, xhr) {
msg_error('Error al consultar')
},
success: function(text, data, xhr){
var values = data.json()
if (values.ok){
agregar_producto(values)
} else {
msg = 'No se encontró la clave<BR><BR>' + key
msg_error(msg)
}
}
})
}
function tsearch_product_key_press(code, e){
var value = this.getValue()
if(code == 13 && value.trim().length > 0){
buscar_producto_key(value.trim())
}
}
function grid_ticket_details_click(id, e, node){
if(id.column != 'delete'){
return
}
var grid = $$('grid_tdetails')
grid.remove(id.row)
}
function grid_tickets_details_before_edit_stop(state, editor){
var grid = $$('grid_tdetails')
var row = grid.getItem(editor.row)
if(editor.column == 'cantidad'){
var cantidad = parseFloat(state.value)
if(isNaN(cantidad)){
msg = 'La cantidad debe ser un número'
msg_error(msg)
grid.blockEvent()
state.value = state.old
grid.editCancel()
grid.unblockEvent()
return true
}
var valor_unitario = parseFloat(row['valor_unitario'])
var descuento = parseFloat(row['descuento'])
}
if(editor.column == 'valor_unitario'){
var valor_unitario = parseFloat(state.value)
if(isNaN(valor_unitario)){
msg = 'El valor unitario debe ser un número'
msg_error(msg)
grid.blockEvent()
state.value = state.old
grid.editCancel()
grid.unblockEvent()
return true
}
var cantidad = parseFloat(row['cantidad'])
var descuento = parseFloat(row['descuento'])
}
if(editor.column == 'descuento'){
var descuento = parseFloat(state.value)
if(isNaN(descuento)){
msg = 'El descuento debe ser un número'
msg_error(msg)
grid.blockEvent()
state.value = state.old
grid.editCancel()
grid.unblockEvent()
return true
}
var cantidad = parseFloat(row['cantidad'])
var valor_unitario = parseFloat(row['valor_unitario'])
}
var precio_final = valor_unitario - descuento
row['importe'] = (cantidad * precio_final).round(DECIMALES)
grid.refresh()
}
function gt_productos_found_click(obj){
buscar_producto_key(obj.clave)
}

View File

@ -55,6 +55,11 @@ function showvar(values){
}
function showtype(values){
webix.message(typeof(values))
}
function show(nombre, value){
if(value == '0'){
value = false
@ -96,7 +101,7 @@ String.prototype.to_float = function(){
function get_float(value){
return parseFloat(value.replace('$', '').replace(',', '').trim()).round(2)
return parseFloat(value.replace('$', '').replace(',', '').trim()).round(DECIMALES)
}
@ -124,12 +129,17 @@ webix.protoUI({
}, webix.ui.text)
webix.ui.datafilter.rowCount = webix.extend({
refresh:function(master, node, value){
node.firstChild.innerHTML = master.count();
}
}, webix.ui.datafilter.summColumn)
//~ webix.ui.datafilter.rowCount = webix.extend({
//~ refresh:function(master, node, value){
//~ node.firstChild.innerHTML = master.count();
//~ }
//~ }, webix.ui.datafilter.summColumn)
webix.ui.datafilter.countRows = webix.extend({
refresh:function(master, node, value){
node.firstChild.innerHTML = master.count();
}
}, webix.ui.datafilter.summColumn);
function validate_rfc(value){
rfc = value.trim().toUpperCase();
@ -291,3 +301,11 @@ webix.DataDriver.plainjs = webix.extend({
return this.hash2tree(hash, 0);
}
}, webix.DataDriver.json)
function get_forma_pago(control){
webix.ajax().get('/values/formapago', {key: true}, function(text, data){
var values = data.json()
$$(control).getList().parse(values)
})
}

View File

@ -2,7 +2,7 @@
var grid_cfdi_cliente_cols = [
{id: 'index', header: '#', adjust: 'data', css: 'right',
footer: {content: 'rowCount', colspan: 3, css: 'right'}},
footer: {content: 'countRows', colspan: 3, css: 'right'}},
{id: "id", header:"ID", hidden:true},
{id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "header",
sort:"string"},
@ -182,7 +182,7 @@ var toolbar_invoices_filter = [
var grid_invoices_cols = [
{id: 'index', header: '#', adjust: 'data', css: 'right',
footer: {content: 'rowCount', colspan: 3, css: 'right'}},
footer: {content: 'countRows', colspan: 3, css: 'right'}},
{id: "id", header:"ID", hidden:true},
{id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data",
sort:"string"},
@ -306,6 +306,7 @@ var suggest_partners = {
}
}
var suggest_products = {
view: 'gridsuggest',
id: 'grid_products_found',

View File

@ -13,7 +13,7 @@ var toolbar_products = [
var grid_products_cols = [
{ id: "id", header: "ID", width: 75, hidden: true},
{ id: "clave", header: ["Clave", {content: "textFilter"}], width: 100,
sort: 'string', footer: {content: 'rowCount', css: 'right'}},
sort: 'string', footer: {content: 'countRows', css: 'right'}},
{ id: "descripcion", header: ["Descripción", {content: "textFilter"}],
fillspace:true, sort: 'string', footer: 'Productos y Servicios'},
{ id: "unidad", header: ["Unidad", {content: "selectFilter"}], width: 150,

View File

@ -21,7 +21,7 @@ var toolbar_tickets_filter = [
var grid_tickets_cols = [
{id: 'index', header: '#', adjust: 'data', css: 'right',
footer: {content: 'rowCount', colspan: 3, css: 'right'}},
footer: {content: 'countRows', colspan: 3, css: 'right'}},
{id: "id", header:"ID", hidden:true},
{id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data",
sort:"string"},
@ -34,7 +34,7 @@ var grid_tickets_cols = [
{id: 'total', header: ['Total', {content: 'numberFilter'}], width: 150,
sort: 'int', format: webix.i18n.priceFormat, css: 'right'},
{id: "cliente", header: ["Razón Social", {content: "selectFilter"}],
fillspace:true, sort:"string"},
fillspace:true, sort:"string", hidden: true},
{id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')},
{id: 'print', header: 'I', adjust: 'data', template: get_icon('print')},
{id: 'email', header: '', adjust: 'data', template: get_icon('email')}
@ -67,37 +67,66 @@ var rows_tickets_home = [
]
var ticket_suggest_products = {
view: 'gridsuggest',
id: 'gt_productos_found',
name: 'gt_productos_found',
body: {
autoConfig: false,
header: true,
columns: [
{id: 'id', hidden: true},
{id: 'clave', header: 'Clave', adjust: 'data'},
{id: 'descripcion', header: 'Descripción', adjust: 'data'},
{id: 'unidad', header: 'Unidad', adjust: 'data'},
{id: 'valor_unitario', header: 'Valor Unitario', adjust: 'data',
format: webix.i18n.priceFormat}
],
dataFeed:function(text){
if (text.length > 2){
this.load('/values/product?name=' + text)
}else{
this.hide()
}
}
}
}
var tbody_buscar_producto = {rows: [
{cols: [
{view: 'search', id: 'tsearch_product_key', name: 'tsearch_product_key',
label: 'por Clave', labelPosition:'top', maxWidth: 250,
placeholder: 'Presiona ENTER para buscar'},
{view: 'search', id: 'tsearch_product_name', name: 'search_product_name',
label: 'por Descripción', labelPosition:'top', suggest: [],
placeholder: 'Captura al menos tres letras'},
{view: 'search', id: 'tsearch_product_name', name: 'tsearch_product_name',
label: 'por Descripción', labelPosition:'top',
suggest: ticket_suggest_products, placeholder: 'Captura al menos tres letras'},
]},
]}
var grid_tdetails_cols = [
{id: "id", header:"ID", hidden: true},
{id: 'delete', header: '', width: 30, css: 'delete'},
{id: "clave", header:{text: 'Clave', css: 'center'}, width: 100},
{id: "clave", header:{text: 'Clave', css: 'center'}, width: 100,
footer: {text: 'Artículos', css: 'right_footer3'}},
{id: "clave_sat", hidden: true},
{id: "descripcion", header:{text: 'Descripción', css: 'center'},
fillspace: true, editor: 'text'},
fillspace: true, footer: {content: 'countRows', css: 'footer3'}},
{id: "unidad", header:{text: 'Unidad', css: 'center'}, width: 100},
{id: 'cantidad', header: {text: 'Cantidad', css: 'center'}, width: 100,
format: webix.i18n.numberFormat, css: 'right', editor: 'text'},
format: webix.i18n.numberFormat, css: 'right', editor: 'text',
footer: {content: 'summColumn', css: 'right_footer3'}},
{id: "valor_unitario", header:{text: 'Valor Unitario', css: 'center'},
width: 100, format: webix.i18n.priceFormat, css: 'right', editor: 'text'},
{id: 'descuento', header:{text: 'Descuento', css: 'center'}, hidden: true,
width: 100, format: webix.i18n.priceFormat, css: 'right',
footer: {text: 'Total ', css: 'right_footer2'}},
{id: 'descuento', header: {text: 'Descuento', css: 'center'}, hidden: true,
width: 80, format: webix.i18n.priceFormat, css: 'right', editor: 'text'},
{id: 'precio_final', hidden: true, header: 'precio_final', width: 80,
format: webix.i18n.priceFormat, css: 'right'},
{id: "importe", header:{text: 'Importe', css: 'center'}, width: 150,
format: webix.i18n.priceFormat, css: 'right'},
format: webix.i18n.priceFormat, css: 'right',
footer: {content: 'summColumn', css: 'right_footer2'}},
]
@ -108,11 +137,26 @@ var grid_tdetails = {
adjust: true,
autoheight: true,
editable: true,
footer: true,
columns: grid_tdetails_cols,
on:{
'data->onStoreUpdated':function(){
this.data.each(function(obj, i){
obj.delete = '-'
})
}
},
data: [],
}
var body_ticket_informacion = {rows: [
{view: 'richselect', id: 'lst_ticket_forma_pago', name: 'forma_pago',
label: 'Forma de Pago', labelPosition: 'top', required: true,
options: []},
],}
var controls_generate_ticket = [
{minHeight: 10, maxHeight: 10},
{cols: [{rows: [
@ -120,7 +164,7 @@ var controls_generate_ticket = [
]},
{maxWidth: 10},
{maxWidth: 300, rows: [
{view: 'fieldset', label: 'Información', body: {}},
{view: 'fieldset', label: 'Información', body: body_ticket_informacion},
]},
]},
{view: 'label', label: 'Detalle', height: 30, align: 'left'},