OSDN Git Service

[REM] inout
[iphwproject/opentck3.git] / opentck3.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 __licence__ = "GNU/GPLv3"
4 __author__ = "Marcelo Zunino (InfoPrimo SL) 2015-2017"
5
6 # from time import strftime
7 import sys
8 import os
9 import logging
10 import csv
11 import json
12 import subprocess
13 import inspect as insp
14
15 from datetime import datetime
16 from typing import Type
17 from decimal import Decimal
18
19 from spazospy.ticket import IPCabezal, IPLinea
20 from chk_json import SqliteTckJson
21 from util import config
22 from parser import parse_lines
23
24
25
26 # fantes = open('antes.json', "w") ; fantes.close()
27 # fahora = open('ahora.json', "w") ; fahora.close()
28
29 ns = 'opentck3'
30 oPath = config.oPath
31 sucursal = '01'
32
33 logfile = './log/opentck3.log'
34 os.path.isdir('./log') or os.mkdir('./log')
35 os.path.isfile(logfile) or os.system("touch %s" % logfile)
36
37 logging.basicConfig(format='%(asctime)s %(message)s', filename=logfile, level=logging.INFO)
38
39
40 # solo para debugging
41 # from inspect import currentframe, getframeinfo
42
43
44 def msgout(msg=None):
45     msg = msg or "\n\tAlgo salio mal... FIN\n"
46     sys.stdout.write(msg, )
47
48
49 def rell(rawcode=None, largo=6):
50     if not rawcode:
51         return '0' * largo
52     else:
53         return (largo - len(str(rawcode))) * '0' + str(rawcode)
54
55
56 def run(command):
57     output = subprocess.check_output(command, shell=True)
58     return output
59
60
61 def get_date(info_file):
62     """ :param info_file: archivo tckAAAAMMDD.tck
63         :return:   texto: una fecha
64
65         Parsea la primera línea del informe creado.
66         si bien el nombre del archivo representa a un fecha, su contenido puede corresponder a una fecha diferente.
67
68     ejemplo de primera línea de informe (siempre será un cabezal de ticket)
69         C#8#1#87#8#20151231080308#F#0#0.00#20#8
70                    |________|
71                    2015/12/31
72
73     :param info_file: archivo tckAAAAMMDD.tck
74     :return:   texto: una fecha
75
76     """
77     count_sep = 0
78     date = ''
79     centinel = 0
80     head = run("head -1 %s" % info_file)
81     head = head.strip('\n')
82
83     for i in range(len(head)):
84         if head[i] == '#':
85             count_sep += 1
86         if count_sep >= 5 and centinel < 9:
87             date += head[i]
88             centinel += 1
89
90     date = date.strip('#')
91     # print date
92     return date
93
94
95
96
97
98 class Excepcion(object):
99
100     stdout_err  = 1  # `stdout_err = 1` mostrará errores en stdout
101     verbose     = 1  # `verbose = 1`    mostrará detalles del error
102     debug_log   = 0  # `debug = 1`      escribe detalles en el log
103     debug_vivo  = 1  # `debug_vivo = 1` abre consola de debug en el error
104
105     def __init__(self, mensaje, frame=None, id_ticket=None):
106         # type: (object, object, object) -> object
107         self.msg = mensaje or " algo salió mal... "
108         self.msgv = ''
109         self.frame = frame
110         self.id_ticket = id_ticket
111
112     def excep(self):
113         """ logging / notificación a stdout """
114         msg = self.msg
115         msgv = self.msgv
116         frame = self.frame
117         id_ticket = self.id_ticket
118         if self.stdout_err and msg:
119             print("\t %s" % (msg,))
120         if self.verbose and frame and id_ticket:
121             msgv = "aaaammdd cja tck: %s" % (id_ticket,)
122             msgv += " en: %s  lín: %s" % (frame.filename, frame.lineno)
123             print("\t %s" % (msgv,))
124         if self.debug_log:
125             msg += msgv
126         msg += " fin ejecución..."
127         if self.debug_vivo:
128             import ipdb
129             ipdb.set_trace()
130         return True
131         # return sys.exit(1)
132
133
134 excepcion = Excepcion  # type: Type[Excepcion]
135
136
137 class OpenTck(object):
138
139     """
140             CSV 2 Python dict
141     """
142
143     def __init__(self, salidapazos):
144
145         if not os.path.isfile(salidapazos):
146             msg = " %s El informe no exixte o no es accesible." % (ns,)
147             logging.error(msg)
148             logging.warn(msg)
149             msgout(msg)
150             sys.exit(1)
151
152         self.salidapazos = salidapazos
153
154     def csv2py(self):
155         """
156             ::PROCESA UN UNICO INFORME::
157             ============================
158             retorna: diccionario:
159                     { cabezal_tck0: linea_tck_pazos0, cabezal_tck1: linea_tck_pazos1, ... }
160             Ejemplo:
161
162             Dada la línea de cabezal: C#1#2#350#2#20151121105326#F#4#183.40#20#13
163             Cabezal_Tck1 = ('1', '2', '350', '2', '20151121105326', 'F', '4', '183.40', '20', '13')
164
165             Dada la línea de detalle: L#4#53#120435#TICKET DE VENTA#0
166             LinTckN_2 = ('4', '53', '120435', 'TICKET DE VENTA', '0')
167
168             En ambos casos se omite el identificador de línea,  "C" o "L"
169         """
170
171         salidapazos = self.salidapazos or None
172         if not salidapazos:
173             msgout("\n\n\t\t\t\t\t   Verificar ingreso ... %s" % ('\n\n',))
174             sys.exit(0)
175         key = None
176         informe = dict()
177         info_line = 0
178
179         with open(salidapazos, 'r') as f:
180             reader = csv.reader(f, delimiter='#')
181             for row in reader:
182                 info_line += 1              # sólo para sber dónde se corta, si es que se corta.
183                 # print '.',
184                 if not len(row):
185                     msg = ' %s La línea está vacía.' % (ns,)
186                     logging.warn(msg)
187                     sys.stdout.write("\n\t%s\n\n" % (msg,))
188                     continue
189                 if row[0] == 'C':
190                     key = tuple(row[1:])    # llave = línea de cabezal sin el primer caracter, o sea sin la "C"
191                     informe[key] = []
192                 elif row[0] == 'L':
193                     informe[key].append(tuple(row[1:]))  # valores = líneas de detalle de ticket
194                                                          # sin la "L"
195                 else:
196                     msgout("\n\n\t\t\t\t\tEl infome salidapazos puede contener erores\n\t\t\t\t\t")
197                     msgout(
198                         "\n\t\t\t\t\tNo se generaron los informes para \'findía\' Ver log.. %s\n\n" % (logfile,))
199                     logging.info(
200                         " %s [ Error ] : Verificar el contenido de %s at line %s" % (ns, salidapazos, info_line))
201                     sys.exit(0)
202         if not informe:
203             msg = " %s [ Error ] : Ocurrió un error al leer informe. " % (ns,)
204             msg += "Verificar %s. No se generó el archivo .json" % (salidapazos,)
205             msgout(msg)
206             logging.info(msg)
207             sys.exit(0)
208
209         return informe     # { tupla:: cabezal_tck0: lista de tuplas:: linea_tck_pazos0, ... }
210
211     def do_cabezales(self, cabezal, lineas_cabezal):
212         return True
213
214     @property
215     def passtck(self):
216         """
217         Procesa los tickets (llama rutina que crea las lineas)
218         tickets = { cabezal_Ticket0: [ lineaTicket0_0, lineaTicket0_1, lineaTicket0_2, lineaTicket0_N ],
219                     cabezal_Ticket1: [ lineaTicket1_0, ... ],
220                     cabezal_TicketN: [ lineaTicketN_0, ....]
221             }
222         cabezal_Ticket :: lista de tuplas
223
224
225         Recibe los tickets del informe y parsea aquellos definidos en la configuración como `cabezales de interés`.
226             1. Instancia la clase `IPCabezal` con los datos de cabezal disponibles.
227                Algunos datos de cabezal para el objeto ERP, deben ser obtenidos en el parseo de líneas pazos.
228             2. Arma una lista con las líneas del ticket definidas como `lineas de interés` en la configuración.
229             3. Con cada ticket invoca al parser de líneas pasándole (cabezal, líneas) como parámetros.
230
231         :return: tcks_del_dia: lista de tuplas [(cant_movimientos, tickets_del_dia)]
232
233         """
234
235         tickets = self.csv2py()
236         cabezales_pazos = tickets.keys()
237         cant_tickets_procesados = 0
238
239         # pos_ticket_id = linea_tck_pazos_id = None
240         tcks_del_dia = list()
241         if 1:  # try:
242             for cabezal in cabezales_pazos:
243                 if cabezal[0] not in config.cabezales_interes.keys():
244                     # saltear tickets que no interesan. solo lee venta/cobr, devol, ndcr
245                     continue
246                 C = IPCabezal(list(cabezal))
247                 # print C.ticket, C.caja ,  C.timestamp
248                 # :debug: reemplazar numero de ticket y nro. de caja
249                 #  if C.ticket == 84174 and C.caja == 1:  import ipdb;ipdb.set_trace()
250                 # print C.llave, C.descripcion
251                 # se CREA "pos_ticket", miembro del cabezal "C"
252                 # extrayendo/simplificando solo datos útiles de "C".
253                 #import ipdb
254                 #ipdb.set_trace()
255                 C.pos_ticket = dict(    # será el cabezal del ticket al que luego se agregarán datos
256                                         # disponibles en las líneas.
257                     # simples
258                     name = C.id_ticket,
259                     sucursal_id = sucursal,
260                     tipocabezal = C.llave,
261                     descripcion_cab = C.descripcion,
262                     timestamptck = datetime.strptime(C.timestamp, "%Y%m%d%H%M%S").strftime('%Y-%m-%d %H:%M:%S'),
263                     codigocaja = C.caja,
264                     numerotck = int(C.ticket),
265                     codigocajera = C.nro_cajera,
266                     date = C.yyyy + '-' + C.mm + '-' + C.dd,
267                     estadotck = C.estado,
268                     cantarticulos = int(C.cant_articulos),
269                     totalapagar = round(Decimal(C.totalapagar), 2),
270                     tipocliente = C.tipo_de_cliente,
271                     cantidadlineas = int(C.cant_lineas),
272                     note = '',
273                     redondeos_tck = list(),
274                     cobranzas_tck = dict(),
275                     # relaciones serán tablas relacionadas al cabezal
276                     mediosdepago = dict(
277                         cuentas_tck = list(),
278                         tarjetas_tck = list(),
279                         cheques_tck = list(),
280                         devenvases_tck = list(),
281                         efectivo_tck = list(),
282                         puntos_tck = list(),
283                         tacrt_tck = list(),
284                         mides_tck = list()
285                         ),
286                     comprobantes = dict(
287                         egresocaja_tck = list(),
288                         retiros_tck = list(),
289                         almuerzo_tck = list(),
290                         ),
291                     )
292
293                 # en el volcado de datos a un ERP deberá redefinirse el concepto de cabzal de ticket
294
295                 # se leen todas las líneas antes de confirmar el cabezal, dado que existen líneas pueden contener
296                 # información relevnte para el cabezal.
297                 if C.llave in ('8', ):
298                     # tickets especiales
299                     # 8 Linea Cajera (entrea-sale-pausa)
300                     continue
301                 lineas_tck_pazos = []
302
303                 for linea in tickets[cabezal]:
304                     # es la llave en la instancia del objeto ticket
305                     if linea[1] not in config.lineas_interes.keys():
306                         continue
307                     lineas_tck_pazos.append(linea)
308                 # print("llave: %s tst: %s  caja: %s  ticket: %s" % (C.llave, ttst14, caja, ticket))
309                 # si hay tickets con líneas.
310                 if isinstance(C, IPCabezal) and len(lineas_tck_pazos):
311
312                     # ############################################################
313                     lineas_ticket = parse_lines(C.pos_ticket, lineas_tck_pazos)
314                     tcks_del_dia.append((C.pos_ticket, lineas_ticket))
315                     try:
316                         # Contar los tickets
317                         if C.pos_ticket['tipocabezal'] == '1' and 'tipo_de_ticket_descrip' in C.pos_ticket:
318                             cant_tickets_procesados += 1
319
320                             # print '%s ' %  (tickets_procesados,),
321                         elif C.pos_ticket['tipocabezal'] in ('3', '16') and 'tipo_de_ticket_descrip' in \
322                                 C.pos_ticket:
323                             # 3 modo devolución se anula algo previamente vendido
324                             # 16 Ticket Nota de Credito Devolucion devol. un ticket de venta con factura.
325                             # Es un cabezal indicador cuyos totales estan en 0.  -ver línea de detalla tipo 92
326                             cant_tickets_procesados -= 1
327
328                             # print '\n\t devol : %s %s %s' %  (cant_tickets_procesados, C.pos_ticket[
329                             # 'tipocabezal'], C.pos_ticket['tipo_de_ticket_descrip']),
330                             # mzs
331                     except Exception as err_msg:
332                         msg = "[ERROR]: Error al parsear: %s %s %s" % (err_msg, C.llave, C.descripcion)
333                         print(msg + ' adentro')
334                         import ipdb;ipdb.set_trace()
335
336                 else:
337                     msg = " %s Ticket informado con errores %s Ver Salidapazos." % (
338                             ns, C.timestamp + C.caja + C.ticket)
339                     logging.warn(msg)
340
341         # except Exception as ex:
342         #    print ex, 'ultimo'
343         #    import ipdb;ipdb.set_trace()
344
345         return cant_tickets_procesados, tcks_del_dia
346
347
348 def main():
349     """
350     #TODO: la desripción de pasos 1-6 es obsoleta 03/11/2019
351     :return: crea los archivo/s .json "tckAAAADDMM.json"
352
353         Procesa informes de salida de los POS, filtra movimientos
354         según criterios configurables, serializa la salida en
355         estructuras `json` que escribe en un archivo.
356
357         1. lee el directorio de salidapazos
358         2. prepara la lista de archivos salidapazos a procesar
359         3. lee los informes del filesystem
360         4. procesa cada informe salidapazos
361         5. crea archivos json correspondientes a cada informe
362         6. marca los informes procesaros en "/admi/pazos4/.sqlite3.db"
363
364         Los archivos json tiene la forma:
365
366         salida = [
367             ({lineas_cabezal},{lineas_detalle}),
368             ({lineas_cabezal},{lineas_detalle}),
369             ({lineas_cabezal},{lineas_detalle})
370         ]
371     """
372
373     # import ipdb;ipdb.set_trace()
374     # ####  1. 2.
375
376     sqlite_inf_obj = SqliteTckJson()
377
378     infofile = sqlite_inf_obj.json_a_procesar()
379     # print len(infofile)
380
381     if not infofile:
382
383         msg = 'No hay informes sin procesar!'
384         print(" %s : %s" % (ns, msg))
385         logging.warn(" %s [ Warninig ] : %s " % (ns, msg))
386         sys.exit(0)
387
388     for i in infofile:
389         fecha = get_date(i)  # es la fecha en que pazos4 emitió el informe.
390         _date = "%s-%s-%s" % (fecha[0:4], fecha[4:6], fecha[6:8])
391         print _date, ' ',
392         # if 1:
393         with Spinner():
394             # ####  3.
395             info_obj = OpenTck(i)
396             cntctk, tcks_del_dia = info_obj.passtck
397
398             # informe = info_obj.csv2py()  # levanta Salidapazos_nuevo (csv) y crea un diccionario
399             # ####  4.
400             # import ipdb;ipdb.set_trace()
401             # cntctk, tcks_del_dia = info_obj.passtck()
402             # tcks_del_dia es un objeto python con todos los tickets de un día/informe
403
404             _date = "%s-%s-%s" % (fecha[0:4], fecha[4:6], fecha[6:8])
405
406             # #### 5.  serializa python to json y escribe el json file
407             json_file = "%stck%s.json" % (oPath, fecha,)
408             try:
409
410                 with open(json_file, 'w') as fp:
411                     json.dump(tcks_del_dia, fp, separators=(',', ': '), indent=3)
412
413                 # #### 6. resgister_json_history_db
414                 sqlite_inf_obj.resgister_json_history_db(i, fecha, json_file)
415                 logging.info(" %s [ Info ] %s generado ok " % (ns, fecha))
416                 sys.stdout.write('\b')
417                 sys.stdout.write(' ')
418                 print ' ', cntctk, 'ok.'
419             except Exception as escritura:
420                 logging.error(" %s [ Error ] : %s Imposible escribir el informe ln 389" % (ns, escritura))
421
422
423 if __name__ == '__main__':
424     main()
425     """
426     json_fil = "/tmp/llave124.json"
427     try:
428         with open(json_fil, 'w') as fp:
429             json.dump(LLAVE124, fp, separators=(',', ': '), indent=3)
430     except Exception as escritura:
431         logging.error( " %s [ Error ] : %s Imposible escribir el informe" % (ns, escritura) )
432     """
433 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: