forked from reingart/pyfiscalprinter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
epsonFiscal.py
406 lines (358 loc) · 17 KB
/
epsonFiscal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# -*- coding: iso-8859-1 -*-
import string
import types
import logging
import unicodedata
from fiscalGeneric import PrinterInterface, PrinterException
import epsonFiscalDriver
class FiscalPrinterError(Exception):
pass
class FileDriver:
def __init__(self, filename):
self.filename = filename
self.file = open(filename, "w")
def sendCommand(self, command, parameters):
self.file.write("Command: %d, Parameters: %s\n" % (command, parameters))
return ["BLA", "BLA", "BLA", "BLA", "BLA", "BLA", "BLA", "BLA", ]
def close(self):
self.file.close()
def formatText(text):
asciiText = unicodedata.normalize('NFKD', unicode(text)).encode('ASCII', 'ignore')
asciiText = asciiText.replace("\t", " ").replace("\n", " ").replace("\r", " ")
return asciiText
class DummyDriver:
def __init__(self):
self.number = int(raw_input("Ingrese el número de la última factura: "))
def close(self):
pass
def sendCommand(self, commandNumber, parameters, skipStatusErrors):
##raise RuntimeError("saraza1")
## if commandNumber in EpsonPrinter.CMD_CLOSE_FISCAL_RECEIPT:
## #raise RuntimeError("saraza")
## else:
## pass
return ["00", "00", "", "", str(self.number), "", str(self.number)] + [str(self.number)] * 11
class EpsonPrinter(PrinterInterface):
DEBUG = True
CMD_OPEN_FISCAL_RECEIPT = 0x40
CMD_OPEN_BILL_TICKET = 0x60
## CMD_PRINT_TEXT_IN_FISCAL = (0x41, 0x61)
CMD_PRINT_TEXT_IN_FISCAL = 0x41
CMD_PRINT_LINE_ITEM = (0x42, 0x62)
CMD_PRINT_SUBTOTAL = (0x43, 0x63)
CMD_ADD_PAYMENT = (0x44, 0x64)
CMD_CLOSE_FISCAL_RECEIPT = (0x45, 0x65)
CMD_DAILY_CLOSE = 0x39
CMD_STATUS_REQUEST = 0x2a
CMD_OPEN_DRAWER = 0x7b
CMD_SET_HEADER_TRAILER = 0x5d
CMD_OPEN_NON_FISCAL_RECEIPT = 0x48
CMD_PRINT_NON_FISCAL_TEXT = 0x49
CMD_CLOSE_NON_FISCAL_RECEIPT = 0x4a
CURRENT_DOC_TICKET = 1
CURRENT_DOC_BILL_TICKET = 2
CURRENT_DOC_CREDIT_TICKET = 4
CURRENT_DOC_NON_FISCAL = 3
models = ["tickeadoras", "epsonlx300+", "tm-220-af"]
def __init__(self, deviceFile=None, speed=9600, host=None, port=None, dummy=False, model=None):
try:
if dummy:
self.driver = DummyDriver()
elif host:
self.driver = epsonFiscalDriver.EpsonFiscalDriverProxy(host, port)
else:
deviceFile = deviceFile or 0
self.driver = epsonFiscalDriver.EpsonFiscalDriver(deviceFile, speed)
#self.driver = FileDriver( "/home/gnarvaja/Desktop/fiscal.txt" )
except Exception, e:
raise FiscalPrinterError("Imposible establecer comunicación.", e)
if not model:
self.model = "tickeadoras"
else:
self.model = model
self._currentDocument = None
self._currentDocumentType = None
def _sendCommand(self, commandNumber, parameters, skipStatusErrors=False):
print "_sendCommand", commandNumber, parameters
try:
logging.getLogger().info("sendCommand: SEND|0x%x|%s|%s" % (commandNumber,
skipStatusErrors and "T" or "F",
str(parameters)))
return self.driver.sendCommand(commandNumber, parameters, skipStatusErrors)
except epsonFiscalDriver.PrinterException, e:
logging.getLogger().error("epsonFiscalDriver.PrinterException: %s" % str(e))
raise PrinterException("Error de la impresora fiscal: " + str(e))
def openNonFiscalReceipt(self):
status = self._sendCommand(self.CMD_OPEN_NON_FISCAL_RECEIPT, [])
self._currentDocument = self.CURRENT_DOC_NON_FISCAL
self._currentDocumentType = None
return status
def printNonFiscalText(self, text):
return self._sendCommand(self.CMD_PRINT_NON_FISCAL_TEXT, [formatText(text[:40] or " ")])
ivaTypeMap = {
PrinterInterface.IVA_TYPE_RESPONSABLE_INSCRIPTO: 'I',
PrinterInterface.IVA_TYPE_RESPONSABLE_NO_INSCRIPTO: 'R',
PrinterInterface.IVA_TYPE_EXENTO: 'E',
PrinterInterface.IVA_TYPE_NO_RESPONSABLE: 'N',
PrinterInterface.IVA_TYPE_CONSUMIDOR_FINAL: 'F',
PrinterInterface.IVA_TYPE_RESPONSABLE_NO_INSCRIPTO_BIENES_DE_USO: 'R',
PrinterInterface.IVA_TYPE_RESPONSABLE_MONOTRIBUTO: 'M',
PrinterInterface.IVA_TYPE_MONOTRIBUTISTA_SOCIAL: 'M',
PrinterInterface.IVA_TYPE_PEQUENIO_CONTRIBUYENTE_EVENTUAL: 'F',
PrinterInterface.IVA_TYPE_PEQUENIO_CONTRIBUYENTE_EVENTUAL_SOCIAL: 'F',
PrinterInterface.IVA_TYPE_NO_CATEGORIZADO: 'F',
}
ADDRESS_SIZE = 30
def _setHeaderTrailer(self, line, text):
self._sendCommand(self.CMD_SET_HEADER_TRAILER, (str(line), text))
def setHeader(self, header=None):
"Establecer encabezados"
if not header:
header = []
line = 3
for text in (header + [chr(0x7f)]*3)[:3]: # Agrego chr(0x7f) (DEL) al final para limpiar las
# líneas no utilizadas
self._setHeaderTrailer(line, text)
line += 1
def setTrailer(self, trailer=None):
"Establecer pie"
if not trailer:
trailer = []
line = 11
for text in (trailer + [chr(0x7f)] * 9)[:9]:
self._setHeaderTrailer(line, text)
line += 1
def openBillCreditTicket(self, type, name, address, doc, docType, ivaType, reference="NC"):
return self._openBillCreditTicket(type, name, address, doc, docType, ivaType, isCreditNote=True)
def openBillTicket(self, type, name, address, doc, docType, ivaType):
return self._openBillCreditTicket(type, name, address, doc, docType, ivaType, isCreditNote=False)
def _openBillCreditTicket(self, type, name, address, doc, docType, ivaType, isCreditNote,
reference=None):
if not doc or filter(lambda x: x not in string.digits + "-.", doc or "") or not \
docType in self.docTypeNames:
doc, docType = "", ""
else:
doc = doc.replace("-", "").replace(".", "")
docType = self.docTypeNames[docType]
self._type = type
if self.model == "epsonlx300+":
parameters = [isCreditNote and "N" or "F", # Por ahora no soporto ND, que sería "D"
"C",
type, # Tipo de FC (A/B/C)
"1", # Copias - Ignorado
"P", # "P" la impresora imprime la lineas(hoja en blanco) o "F" preimpreso
"17", # Tamaño Carac - Ignorado
"I", # Responsabilidad en el modo entrenamiento - Ignorado
self.ivaTypeMap.get(ivaType, "F"), # Iva Comprador
formatText(name[:40]), # Nombre
formatText(name[40:80]), # Segunda parte del nombre - Ignorado
formatText(docType) or (isCreditNote and "-" or ""),
# Tipo de Doc. - Si es NC obligado pongo algo
doc or (isCreditNote and "-" or ""), # Nro Doc - Si es NC obligado pongo algo
"N", # No imprime leyenda de BIENES DE USO
formatText(address[:self.ADDRESS_SIZE] or "-"), # Domicilio
formatText(address[self.ADDRESS_SIZE:self.ADDRESS_SIZE * 2]), # Domicilio 2da linea
formatText(address[self.ADDRESS_SIZE * 2:self.ADDRESS_SIZE * 3]), # Domicilio 3ra linea
(isCreditNote or self.ivaTypeMap.get(ivaType, "F") != "F") and "-" or "",
# Remito primera linea - Es obligatorio si el cliente no es consumidor final
"", # Remito segunda linea
"C", # No somos una farmacia
]
else:
parameters = [isCreditNote and "M" or "T", # Ticket NC o Factura
"C", # Tipo de Salida - Ignorado
type, # Tipo de FC (A/B/C)
"1", # Copias - Ignorado
"P", # Tipo de Hoja - Ignorado
"17", # Tamaño Carac - Ignorado
"E", # Responsabilidad en el modo entrenamiento - Ignorado
self.ivaTypeMap.get(ivaType, "F"), # Iva Comprador
formatText(name[:40]), # Nombre
formatText(name[40:80]), # Segunda parte del nombre - Ignorado
formatText(docType) or (isCreditNote and "-" or ""),
# Tipo de Doc. - Si es NC obligado pongo algo
doc or (isCreditNote and "-" or ""), # Nro Doc - Si es NC obligado pongo algo
"N", # No imprime leyenda de BIENES DE USO
formatText(address[:self.ADDRESS_SIZE] or "-"), # Domicilio
formatText(address[self.ADDRESS_SIZE:self.ADDRESS_SIZE * 2]), # Domicilio 2da linea
formatText(address[self.ADDRESS_SIZE * 2:self.ADDRESS_SIZE * 3]), # Domicilio 3ra linea
(isCreditNote or self.ivaTypeMap.get(ivaType, "F") != "F") and "-" or "",
# Remito primera linea - Es obligatorio si el cliente no es consumidor final
"", # Remito segunda linea
"C", # No somos una farmacia
]
if isCreditNote:
self._currentDocument = self.CURRENT_DOC_CREDIT_TICKET
else:
self._currentDocument = self.CURRENT_DOC_BILL_TICKET
# guardo el tipo de FC (A/B/C)
self._currentDocumentType = type
return self._sendCommand(self.CMD_OPEN_BILL_TICKET, parameters)
def _getCommandIndex(self):
if self._currentDocument == self.CURRENT_DOC_TICKET:
return 0
elif self._currentDocument in (self.CURRENT_DOC_BILL_TICKET, self.CURRENT_DOC_CREDIT_TICKET):
return 1
elif self._currentDocument == self.CURRENT_DOC_NON_FISCAL:
return 2
raise "Invalid currentDocument"
def openTicket(self, defaultLetter='B'):
if self.model == "epsonlx300+":
return self.openBillTicket(defaultLetter, "CONSUMIDOR FINAL", "", None, None,
self.IVA_TYPE_CONSUMIDOR_FINAL)
else:
self._sendCommand(self.CMD_OPEN_FISCAL_RECEIPT, ["C"])
self._currentDocument = self.CURRENT_DOC_TICKET
def openDrawer(self):
self._sendCommand(self.CMD_OPEN_DRAWER, [])
def closeDocument(self):
if self._currentDocument == self.CURRENT_DOC_TICKET:
reply = self._sendCommand(self.CMD_CLOSE_FISCAL_RECEIPT[self._getCommandIndex()], ["T"])
return reply[2]
if self._currentDocument == self.CURRENT_DOC_BILL_TICKET:
reply = self._sendCommand(self.CMD_CLOSE_FISCAL_RECEIPT[self._getCommandIndex()],
[self.model == "epsonlx300+" and "F" or "T", self._type, "FINAL"])
del self._type
return reply[2]
if self._currentDocument == self.CURRENT_DOC_CREDIT_TICKET:
reply = self._sendCommand(self.CMD_CLOSE_FISCAL_RECEIPT[self._getCommandIndex()],
[self.model == "epsonlx300+" and "N" or "M", self._type, "FINAL"])
del self._type
return reply[2]
if self._currentDocument in (self.CURRENT_DOC_NON_FISCAL, ):
return self._sendCommand(self.CMD_CLOSE_NON_FISCAL_RECEIPT, ["T"])
raise NotImplementedError
def cancelDocument(self):
if self._currentDocument in (self.CURRENT_DOC_TICKET, self.CURRENT_DOC_BILL_TICKET,
self.CURRENT_DOC_CREDIT_TICKET):
status = self._sendCommand(self.CMD_ADD_PAYMENT[self._getCommandIndex()], ["Cancelar", "0", 'C'])
return status
if self._currentDocument in (self.CURRENT_DOC_NON_FISCAL, ):
self.printNonFiscalText("CANCELADO")
return self.closeDocument()
raise NotImplementedError
def addItem(self, description, quantity, price, iva, discount, discountDescription, negative=False):
if type(description) in types.StringTypes:
description = [description]
if negative:
sign = 'R'
else:
sign = 'M'
quantityStr = str(int(quantity * 1000))
if self.model == "epsonlx300+":
bultosStr = str(int(quantity))
else:
bultosStr = "0" * 5 # No se usa en TM220AF ni TM300AF ni TMU220AF
if self._currentDocumentType != 'A':
# enviar con el iva incluido
priceUnitStr = str(int(round(price * 100, 0)))
else:
if self.model == "tm-220-af":
# enviar sin el iva (factura A)
priceUnitStr = "%0.4f" % (price / ((100.0 + iva) / 100.0))
else:
# enviar sin el iva (factura A)
priceUnitStr = str(int(round((price / ((100 + iva) / 100)) * 100, 0)))
ivaStr = str(int(iva * 100))
extraparams = self._currentDocument in (self.CURRENT_DOC_BILL_TICKET,
self.CURRENT_DOC_CREDIT_TICKET) and ["", "", ""] or []
if self._getCommandIndex() == 0:
for d in description[:-1]:
self._sendCommand(self.CMD_PRINT_TEXT_IN_FISCAL,
[formatText(d)[:20]])
reply = self._sendCommand(self.CMD_PRINT_LINE_ITEM[self._getCommandIndex()],
[formatText(description[-1][:20]),
quantityStr, priceUnitStr, ivaStr, sign, bultosStr, "0" * 8] + extraparams)
if discount:
discountStr = str(int(discount * 100))
self._sendCommand(self.CMD_PRINT_LINE_ITEM[self._getCommandIndex()],
[formatText(discountDescription[:20]), "1000",
discountStr, ivaStr, 'R', "0", "0"] + extraparams)
return reply
def addPayment(self, description, payment):
paymentStr = str(int(payment * 100))
status = self._sendCommand(self.CMD_ADD_PAYMENT[self._getCommandIndex()],
[formatText(description)[:20], paymentStr, 'T'])
return status
def addAdditional(self, description, amount, iva, negative=False):
"""Agrega un adicional a la FC.
@param description Descripción
@param amount Importe (sin iva en FC A, sino con IVA)
@param iva Porcentaje de Iva
@param negative True->Descuento, False->Recargo"""
if negative:
sign = 'R'
else:
sign = 'M'
quantityStr = "1000"
bultosStr = "0"
priceUnit = amount
if self._currentDocumentType != 'A':
# enviar con el iva incluido
priceUnitStr = str(int(round(priceUnit * 100, 0)))
else:
# enviar sin el iva (factura A)
priceUnitStr = str(int(round((priceUnit / ((100 + iva) / 100)) * 100, 0)))
ivaStr = str(int(iva * 100))
extraparams = self._currentDocument in (self.CURRENT_DOC_BILL_TICKET,
self.CURRENT_DOC_CREDIT_TICKET) and ["", "", ""] or []
reply = self._sendCommand(self.CMD_PRINT_LINE_ITEM[self._getCommandIndex()],
[formatText(description[:20]),
quantityStr, priceUnitStr, ivaStr, sign, bultosStr, "0"] + extraparams)
return reply
def dailyClose(self, type):
reply = self._sendCommand(self.CMD_DAILY_CLOSE, [type, "P"])
return reply[2:]
def getLastNumber(self, letter):
reply = self._sendCommand(self.CMD_STATUS_REQUEST, ["A"], True)
if len(reply) < 3:
# La respuesta no es válida. Vuelvo a hacer el pedido y si hay algún error que se reporte como excepción
reply = self._sendCommand(self.CMD_STATUS_REQUEST, ["A"], False)
if letter == "A":
return int(reply[6])
else:
return int(reply[4])
def getLastCreditNoteNumber(self, letter):
reply = self._sendCommand(self.CMD_STATUS_REQUEST, ["A"], True)
if len(reply) < 3:
# La respuesta no es válida. Vuelvo a hacer el pedido y si hay algún error que se reporte como excepción
reply = self._sendCommand(self.CMD_STATUS_REQUEST, ["A"], False)
if letter == "A":
return int(reply[10])
else:
return int(reply[11])
def cancelAnyDocument(self):
try:
self._sendCommand(self.CMD_ADD_PAYMENT[0], ["Cancelar", "0", 'C'])
return True
except:
pass
try:
self._sendCommand(self.CMD_ADD_PAYMENT[1], ["Cancelar", "0", 'C'])
return True
except:
pass
try:
self._sendCommand(self.CMD_CLOSE_NON_FISCAL_RECEIPT, ["T"])
return True
except:
pass
return False
def getWarnings(self):
ret = []
reply = self._sendCommand(self.CMD_STATUS_REQUEST, ["N"], True)
printerStatus = reply[0]
x = int(printerStatus, 16)
if ((1 << 4) & x) == (1 << 4):
ret.append("Poco papel para la cinta de auditoría")
if ((1 << 5) & x) == (1 << 5):
ret.append("Poco papel para comprobantes o tickets")
return ret
def __del__(self):
try:
self.close()
except:
pass
def close(self):
self.driver.close()
self.driver = None