diff --git a/source/app/models/db.py b/source/app/models/db.py
index 89df7f6..7a48236 100644
--- a/source/app/models/db.py
+++ b/source/app/models/db.py
@@ -263,6 +263,8 @@ class StorageEngine(object):
return main.Tickets.add(values)
if opt == 'cancel':
return main.Tickets.cancel(values)
+ if opt == 'invoice':
+ return main.Tickets.invoice(values)
def get_tickets(self, values):
return main.Tickets.get_by(values)
diff --git a/source/app/models/main.py b/source/app/models/main.py
index 803427f..46a1683 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -1790,7 +1790,7 @@ class Socios(BaseModel):
fields = cls._clean(cls, values)
try:
obj = Socios.create(**fields)
- except IntegrityError:
+ except IntegrityError as e:
msg = 'Ya existe el RFC y Razón Social'
data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
return data
@@ -2789,17 +2789,9 @@ class Facturas(BaseModel):
totals_tax[tax.id] = tax
for tax in totals_tax.values():
- # ~ if tax.tipo == 'E' or tax.tipo == 'R':
if tax.tipo == 'E':
continue
- # ~ import_tax = round(float(tax.tasa) * tax.importe, DECIMALES)
- # ~ if tax.key == '000':
- # ~ locales_traslados += import_tax
- # ~ else:
- # ~ total_trasladados = (total_trasladados or 0) + import_tax
- # ~ if tax.name == 'IVA':
- # ~ total_iva += import_tax
invoice_tax = {
'factura': invoice.id,
@@ -2809,26 +2801,6 @@ class Facturas(BaseModel):
}
FacturasImpuestos.create(**invoice_tax)
- # ~ for tax in totals_tax.values():
- # ~ if tax.tipo == 'E' or tax.tipo == 'T':
- # ~ continue
- # ~ if tax.tasa == round(Decimal(2/3), 6):
- # ~ import_tax = round(float(tax.tasa) * total_iva, DECIMALES)
- # ~ else:
- # ~ import_tax = round(float(tax.tasa) * tax.importe, DECIMALES)
- # ~ if tax.key == '000':
- # ~ locales_retenciones += import_tax
- # ~ else:
- # ~ total_retenciones = (total_retenciones or 0) + import_tax
-
- # ~ invoice_tax = {
- # ~ 'factura': invoice.id,
- # ~ 'impuesto': tax.id,
- # ~ 'base': tax.base,
- # ~ 'importe': tax.suma_impuestos,
- # ~ }
- # ~ FacturasImpuestos.create(**invoice_tax)
-
total = subtotal - descuento_cfdi + \
(total_trasladados or 0) - (total_retenciones or 0) \
+ locales_traslados - locales_retenciones
@@ -4006,6 +3978,162 @@ class Tickets(BaseModel):
data = {'ok': True, 'row': row}
return data
+ def _get_folio_invoice(self, serie):
+ inicio = (Facturas
+ .select(fn.Max(Facturas.folio).alias('mf'))
+ .where(Facturas.serie==serie)
+ .order_by(SQL('mf'))
+ .scalar())
+
+ if inicio is None:
+ inicio = 1
+ else:
+ inicio += 1
+
+ return inicio
+
+ def _cancel_tickets(self, invoice, tickets):
+ query = (Tickets
+ .update(estatus='Facturado', cancelado=True, factura=invoice)
+ .where(Tickets.id.in_(tickets))
+ )
+ result = query.execute()
+ print (result)
+ return
+
+ def _calculate_totals_invoice(self, invoice, tickets):
+ subtotal = 0
+ descuento_cfdi = 0
+ totals_tax = {}
+ total_trasladados = None
+ total_retenciones = None
+
+ details = TicketsDetalle.select().where(TicketsDetalle.ticket.in_(tickets))
+
+ for detail in details:
+ product = {}
+ p = detail.producto
+ product['unidad'] = p.unidad.key
+ product['clave'] = p.clave
+ product['clave_sat'] = p.clave_sat
+
+ product['factura'] = invoice.id
+ product['producto'] = p.id
+ product['descripcion'] = detail.descripcion
+
+ cantidad = float(detail.cantidad)
+ valor_unitario = float(detail.valor_unitario)
+ descuento = float(detail.descuento)
+ precio_final = valor_unitario - descuento
+ importe = round(cantidad * precio_final, DECIMALES)
+
+ product['cantidad'] = cantidad
+ product['valor_unitario'] = valor_unitario
+ product['descuento'] = round(descuento * cantidad, DECIMALES)
+ product['precio_final'] = precio_final
+ product['importe'] = round(cantidad * valor_unitario, DECIMALES)
+
+ descuento_cfdi += product['descuento']
+ subtotal += product['importe']
+
+ FacturasDetalle.create(**product)
+
+ base = product['importe'] - product['descuento']
+ 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
+ elif tax.tipo == 'R' and tax.key != '000':
+ total_retenciones = (total_retenciones or 0) + impuesto_producto
+ elif tax.tipo == 'T' and tax.key == '000':
+ locales_traslados += impuesto_producto
+ elif tax.tipo == 'R' and tax.key == '000':
+ locales_retenciones += impuesto_producto
+
+ if tax.id in totals_tax:
+ totals_tax[tax.id].base += base
+ totals_tax[tax.id].suma_impuestos += impuesto_producto
+ else:
+ tax.base = base
+ tax.suma_impuestos = impuesto_producto
+ totals_tax[tax.id] = tax
+
+ for tax in totals_tax.values():
+ if tax.tipo == 'E':
+ continue
+
+
+ invoice_tax = {
+ 'factura': invoice.id,
+ 'impuesto': tax.id,
+ 'base': tax.base,
+ 'importe': tax.suma_impuestos,
+ }
+ FacturasImpuestos.create(**invoice_tax)
+
+ total = subtotal - descuento_cfdi + \
+ (total_trasladados or 0) - (total_retenciones or 0)
+ total_mn = round(total * invoice.tipo_cambio, DECIMALES)
+ data = {
+ 'subtotal': subtotal,
+ 'descuento': descuento_cfdi,
+ 'total': total,
+ 'total_mn': total_mn,
+ 'total_trasladados': total_trasladados,
+ 'total_retenciones': total_retenciones,
+ }
+ return data
+
+ @classmethod
+ def invoice(cls, values):
+ is_invoice_day = util.get_bool(values['is_invoice_day'])
+ id_client = int(values['client'])
+ tickets = util.loads(values['tickets'])
+
+ if is_invoice_day:
+ filters = (
+ Socios.rfc == 'XAXX010101000' and
+ Socios.slug == 'publico_en_general')
+ try:
+ client = Socios.get(filters)
+ except Socios.DoesNotExist:
+ msg = 'No existe el cliente Público en General. Agregalo primero.'
+ data = {'ok': False, 'msg': msg}
+ return data
+ else:
+ client = Socios.get(Socios.id==id_client)
+
+ emisor = Emisor.select()[0]
+ data = {}
+ data['cliente'] = client
+ data['serie'] = 'T'
+ data['folio'] = cls._get_folio_invoice(cls, data['serie'])
+ data['forma_pago'] = client.forma_pago.key
+ data['tipo_cambio'] = 1.00
+ data['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal
+ if client.uso_cfdi is None:
+ data['uso_cfdi'] = 'P01'
+ else:
+ data['uso_cfdi'] = client.uso_cfdi.key
+ data['regimen_fiscal'] = emisor.regimenes[0].key
+
+ with database_proxy.atomic() as txn:
+ obj = Facturas.create(**data)
+ totals = cls._calculate_totals_invoice(cls, obj, tickets)
+ obj.subtotal = totals['subtotal']
+ obj.descuento = totals['descuento']
+ obj.total_trasladados = totals['total_trasladados']
+ obj.total_retenciones = totals['total_retenciones']
+ obj.total = totals['total']
+ obj.saldo = totals['total']
+ obj.total_mn = totals['total_mn']
+ obj.save()
+ cls._cancel_tickets(cls, obj, tickets)
+
+ msg = 'Factura generada correctamente.
Enviando a timbrar'
+ data = {'ok': True, 'msg': msg, 'id': obj.id}
+ return data
+
@classmethod
def cancel(cls, values):
id = int(values['id'])
@@ -4021,6 +4149,10 @@ class Tickets(BaseModel):
if not opt:
return
+ if opt == 'active':
+ filters = (Tickets.cancelado==False)
+ return filters
+
if opt == 'today':
t = util.today()
filters = (
diff --git a/source/static/js/controller/partners.js b/source/static/js/controller/partners.js
index 93b4346..74185e4 100644
--- a/source/static/js/controller/partners.js
+++ b/source/static/js/controller/partners.js
@@ -155,7 +155,7 @@ function cmd_save_partner_click(id, e, node){
if (values.ok) {
update_grid_partner(values)
} else {
- msg_error(msg)
+ msg_error(values.msg)
}
}
})
diff --git a/source/static/js/controller/tickets.js b/source/static/js/controller/tickets.js
index 74060ca..741b5f1 100644
--- a/source/static/js/controller/tickets.js
+++ b/source/static/js/controller/tickets.js
@@ -11,6 +11,8 @@ var tickets_controllers = {
$$('cmd_new_invoice_from_ticket').attachEvent('onItemClick', cmd_new_invoice_from_ticket_click)
$$('cmd_close_ticket_invoice').attachEvent('onItemClick', cmd_cerrar_ticket_click)
$$('cmd_cancelar_ticket').attachEvent('onItemClick', cmd_cancelar_ticket_click)
+ $$('cmd_move_tickets_right').attachEvent('onItemClick', cmd_move_tickets_right_click)
+ $$('cmd_move_tickets_left').attachEvent('onItemClick', cmd_move_tickets_left_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)
@@ -18,8 +20,13 @@ var tickets_controllers = {
$$('filter_year_ticket').attachEvent('onChange', filter_year_ticket_change)
$$('filter_month_ticket').attachEvent('onChange', filter_month_ticket_change)
$$('chk_is_invoice_day').attachEvent('onChange', chk_is_invoice_day_change)
+ $$('grid_tickets_active').attachEvent('onItemDblClick', grid_tickets_active_double_click)
+ $$('grid_tickets_invoice').attachEvent('onItemDblClick', grid_tickets_invoice_double_click)
+ $$('tsearch_client_key').attachEvent('onKeyPress', tsearch_client_key_press)
+ $$('grid_ticket_clients_found').attachEvent('onValueSuggest', grid_ticket_clients_found_click)
webix.extend($$('grid_tickets'), webix.ProgressBar)
+ webix.extend($$('grid_tickets_active'), webix.ProgressBar)
}
}
@@ -87,8 +94,38 @@ function configuracion_inicial_ticket(){
}
+function get_active_tickets(grid){
+ filters = {'opt': 'active'}
+ grid.showProgress({type: 'icon'})
+
+ webix.ajax().get('/tickets', filters, {
+ error: function(text, data, xhr) {
+ msg_error('Error al consultar')
+ },
+ success: function(text, data, xhr) {
+ var values = data.json();
+ grid.clearAll();
+ if (values.ok){
+ grid.parse(values.rows, 'json')
+ }
+ }
+ })
+}
+
+
function configuracion_inicial_ticket_to_invoice(){
- //~ get_active_tickets()
+ var grid = $$('grid_tickets_active')
+ var gridt = $$('grid_tickets_invoice')
+ var form = $$('form_ticket_invoice')
+
+ get_active_tickets(grid)
+ form.setValues({id_partner: 0, lbl_tclient: 'Ninguno'})
+ gridt.attachEvent('onAfterAdd', function(id, index){
+ gridt.adjustColumn('index')
+ gridt.adjustColumn('folio', 'all')
+ gridt.adjustColumn('fecha', 'all')
+ });
+ gridt.clearAll()
}
@@ -416,6 +453,165 @@ function chk_is_invoice_day_change(new_value, old_value){
}
+function send_timbrar_invoice(id){
+ webix.ajax().get('/values/timbrar', {id: id}, function(text, data){
+ var values = data.json()
+ if(values.ok){
+ msg_ok(values.msg)
+ }else{
+ webix.alert({
+ title: 'Error al Timbrar',
+ text: values.msg,
+ type: 'alert-error'
+ })
+ }
+ })
+
+}
+
+
+function save_ticket_to_invoice(data){
+ 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(values.msg)
+ send_timbrar_invoice(values.id)
+ $$('multi_tickets').setValue('tickets_home')
+ }else{
+ msg_error(values.msg)
+ }
+ }
+ })
+}
+
+
function cmd_new_invoice_from_ticket_click(){
- showvar('ok')
+ var form = this.getFormView();
+ var chk = $$('chk_is_invoice_day')
+ var grid = $$('grid_tickets_invoice')
+ var values = form.getValues()
+ var tickets = []
+
+ if(!chk.getValue()){
+ if(values.id_partner == 0){
+ webix.UIManager.setFocus('tsearch_client_name')
+ msg = 'Selecciona un cliente'
+ msg_error(msg)
+ return false
+ }
+ }
+
+ if(!grid.count()){
+ msg = 'Agrega al menos un ticket a facturar'
+ msg_error(msg)
+ return false
+ }
+
+ grid.eachRow(function(row){
+ tickets.push(row)
+ })
+
+ var data = new Object()
+ data['client'] = values.id_partner
+ data['tickets'] = tickets
+ data['is_invoice_day'] = chk.getValue()
+ data['opt'] = 'invoice'
+
+ msg = 'Todos los datos son correctos.
¿Estás seguro de generar esta factura?'
+ webix.confirm({
+ title: 'Generar Factura',
+ ok: 'Si',
+ cancel: 'No',
+ type: 'confirm-error',
+ text: msg,
+ callback:function(result){
+ if(result){
+ save_ticket_to_invoice(data)
+ }
+ }
+ })
+}
+
+
+function grid_tickets_active_double_click(id, e, node){
+ this.move(id.row, -1, $$('grid_tickets_invoice'))
+}
+
+
+function grid_tickets_invoice_double_click(id, e, node){
+ this.move(id.row, -1, $$('grid_tickets_active'))
+}
+
+
+function cmd_move_tickets_right_click(){
+ $$('grid_tickets_active').eachRow(
+ function(row){
+ this.copy(row, -1, $$('grid_tickets_invoice'))
+ }
+ )
+ $$('grid_tickets_active').clearAll()
+}
+
+
+function cmd_move_tickets_left_click(){
+ $$('grid_tickets_invoice').eachRow(
+ function(row){
+ this.copy(row, -1, $$('grid_tickets_active'))
+ }
+ )
+ $$('grid_tickets_invoice').clearAll()
+}
+
+
+function ticket_set_client(row){
+ var form = $$('form_ticket_invoice')
+ var html = ''
+ form.setValues({
+ id_partner: row.id,
+ tsearch_client_key: '',
+ tsearch_client_name: ''}, true)
+ html += row.nombre + ' (' + row.rfc + ')'
+ $$('lbl_tclient').setValue(html)
+}
+
+
+function ticket_search_client_by_id(id){
+ webix.ajax().get('/values/client', {'id': id}, {
+ error: function(text, data, xhr) {
+ msg_error('Error al consultar')
+ },
+ success: function(text, data, xhr){
+ var values = data.json()
+ if (values.ok){
+ ticket_set_client(values.row)
+ }else{
+ msg = 'No se encontró un cliente con la clave: ' + id
+ msg_error(msg)
+ }
+ }
+ })
+
+}
+
+
+function tsearch_client_key_press(code, e){
+ var value = this.getValue()
+ if(code == 13 && value.length > 0){
+ var id = parseInt(value, 10)
+ if (isNaN(id)){
+ msg_error('Captura una clave válida')
+ }else{
+ ticket_search_client_by_id(id)
+ }
+ }
+}
+
+
+function grid_ticket_clients_found_click(obj){
+ ticket_set_client(obj)
}
\ No newline at end of file
diff --git a/source/static/js/ui/tickets.js b/source/static/js/ui/tickets.js
index 5d2df46..609a847 100644
--- a/source/static/js/ui/tickets.js
+++ b/source/static/js/ui/tickets.js
@@ -200,10 +200,10 @@ var toolbar_ticket_invoice = {view: 'toolbar', elements: [{},
{}]}
-var tsuggest_partners = {
+var ticket_suggest_partners = {
view: 'gridsuggest',
- id: 'grid_tclients_found',
- name: 'grid_tclients_found',
+ id: 'grid_ticket_clients_found',
+ name: 'grid_ticket_clients_found',
body: {
autoConfig: false,
header: false,
@@ -233,7 +233,7 @@ var ticket_search_client = {cols: [{rows: [
placeholder:'Presiona ENTER para buscar'},
{view: 'search', id: 'tsearch_client_name',
name: 'tsearch_client_name', label: 'por Nombre o RFC',
- labelPosition: 'top', suggest: tsuggest_partners,
+ labelPosition: 'top', suggest: ticket_suggest_partners,
placeholder: 'Captura al menos tres letras'},
]},
{cols: [
@@ -247,17 +247,17 @@ var ticket_search_client = {cols: [{rows: [
var grid_tickets_active_cols = [
- {id: 'index', header: '#', adjust: 'data', css: 'right',
- footer: {content: 'countRows', colspan: 3, css: 'right'}},
+ {id: 'index', header: '#', adjust: 'data', css: 'right'},
{id: "id", header:"ID", hidden:true},
{id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data",
- sort:"string", hidden: true},
+ sort: "string", hidden: true},
{id: 'folio', header: ['Folio', {content: 'numberFilter'}], adjust: 'header',
- sort: 'int', css: 'right', footer: {text: 'Tickets', colspan: 3}},
- {id: "fecha", header: ["Fecha y Hora"],
- adjust: "data", sort: "string"},
- {id: 'total', header: 'Total', width: 150,sort: 'int',
- format: webix.i18n.priceFormat, css: 'right'},
+ sort: 'int', css: 'right', footer: {content: 'countRows', css: 'right'}},
+ {id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort: "string",
+ footer: 'Tickets'},
+ {id: 'total', header: 'Total', width: 150,sort: 'int', css: 'right',
+ format: webix.i18n.priceFormat, footer: {content: 'summColumn',
+ css: 'right'}},
]
@@ -267,6 +267,7 @@ var grid_tickets_active = {
select: 'row',
adjust: true,
footer: true,
+ drag: true,
resizeColumn: true,
headermenu: true,
columns: grid_tickets_active_cols,
@@ -281,17 +282,17 @@ var grid_tickets_active = {
var grid_tickets_invoice_cols = [
- {id: 'index', header: '#', adjust: 'data', css: 'right',
- footer: {content: 'countRows', colspan: 3, css: 'right'}},
+ {id: 'index', header: '#', adjust: 'data', css: 'right'},
{id: "id", header:"ID", hidden:true},
{id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data",
- sort:"string", hidden: true},
- {id: 'folio', header: 'Folio', adjust: 'header', sort: 'int',
- css: 'right', footer: {text: 'Tickets', colspan: 3}},
- {id: "fecha", header: ["Fecha y Hora"],
- adjust: "data", sort: "string"},
- {id: 'total', header: 'Total', width: 150,sort: 'int',
- format: webix.i18n.priceFormat, css: 'right'},
+ sort: "string", hidden: true},
+ {id: 'folio', header: ['Folio', {content: 'numberFilter'}], adjust: 'header',
+ sort: 'int', css: 'right', footer: {content: 'countRows', css: 'right'}},
+ {id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort: "string",
+ footer: 'Tickets'},
+ {id: 'total', header: 'Total', width: 150,sort: 'int', css: 'right',
+ format: webix.i18n.priceFormat, footer: {content: 'summColumn',
+ css: 'right'}},
]
@@ -301,6 +302,7 @@ var grid_tickets_invoice = {
select: 'row',
adjust: true,
footer: true,
+ drag: true,
resizeColumn: true,
headermenu: true,
columns: grid_tickets_invoice_cols,
@@ -321,8 +323,13 @@ var controls_ticket_to_invoice = [
ticket_search_client,
{minHeight: 5, maxHeight: 5},
{cols:[
- {rows: [{view: 'label', label: 'Tickets sin facturar', height: 30, align: 'left'}, grid_tickets_active]},
- {minWidth: 10, maxWidth: 10},
+ {rows: [{view: 'label', label: 'Tickets sin facturar', height: 30,
+ align: 'left'},
+ grid_tickets_active]},
+ {rows:[{},
+ {view: 'button', id: 'cmd_move_tickets_right', label: '->', autowidth: true},
+ {view: 'button', id: 'cmd_move_tickets_left', label: '<-', autowidth: true},
+ {}]},
{rows: [{view: 'label', label: 'Tickets a facturar', height: 30, align: 'left'}, grid_tickets_invoice]},
]},
{minHeight: 20, maxHeight: 20},