~georgeyk/stoqdrivers/scales

« back to all changes in this revision

Viewing changes to stoqdrivers/printers/perto/Pay2023.py

  • Committer: romaia
  • Date: 2009-05-11 14:05:48 UTC
  • Revision ID: vcs-imports@canonical.com-20090511140548-edzj9yxbdhlu7b80
adicionando suporte para impressora dataregis quick (fiscnet)

Show diffs side-by-side

added added

removed removed

Lines of Context:
28
28
PertoPay 2023 driver implementation.
29
29
"""
30
30
 
31
 
import datetime
32
 
from decimal import Decimal
33
 
import re
34
 
 
35
 
from kiwi.python import Settable
36
 
from serial import PARITY_EVEN
37
 
from zope.interface import implements
38
 
 
39
 
from stoqdrivers.serialbase import SerialBase
40
 
from stoqdrivers.interfaces import (ICouponPrinter,
41
 
                                            IChequePrinter)
42
 
from stoqdrivers.printers.cheque import (BaseChequePrinter,
43
 
                                                 BankConfiguration)
44
 
from stoqdrivers.printers.base import BaseDriverConstants
45
 
from stoqdrivers.enum import PaymentMethodType, TaxType, UnitType
46
 
from stoqdrivers.exceptions import (
47
 
    DriverError, PendingReduceZ, CommandParametersError, CommandError,
48
 
    ReadXError, OutofPaperError, CouponTotalizeError, PaymentAdditionError,
49
 
    CancelItemError, CouponOpenError, InvalidState, PendingReadX,
50
 
    CloseCouponError, CouponNotOpenError)
51
 
from stoqdrivers.printers.capabilities import Capability
 
31
from stoqdrivers.printers.fiscnet.FiscNetECF import FiscNetChequePrinter
52
32
from stoqdrivers.translation import stoqdrivers_gettext
53
33
 
54
34
_ = stoqdrivers_gettext
55
35
 
56
 
# Page 92
57
 
[FLAG_INTERVENCAO_TECNICA,
58
 
 FLAG_SEM_MFD,
59
 
 FLAG_RAM_NOK,
60
 
 FLAG_RELOGIO_NOK,
61
 
 FLAG_SEM_MF,
62
 
 FLAG_DIA_FECHADO,
63
 
 FLAG_DIA_ABERTO,
64
 
 FLAG_Z_PENDENTE,
65
 
 FLAG_SEM_PAPEL,
66
 
 FLAG_MECANISM_NOK,
67
 
 FLAG_DOCUMENTO_ABERTO,
68
 
 FLAG_INSCRICOES_OK,
69
 
 FLAG_CLICHE_OK,
70
 
 FLAG_EM_LINHA,
71
 
 FLAG_MFD_ESGOTADA] = _status_flags = [2**n for n in range(15)]
72
 
 
73
 
_flagnames = {
74
 
    FLAG_INTERVENCAO_TECNICA: 'FLAG_INTERVENCAO_TECNICA',
75
 
    FLAG_SEM_MFD: 'FLAG_SEM_MFD',
76
 
    FLAG_RAM_NOK: 'FLAG_RAM_NOK',
77
 
    FLAG_RELOGIO_NOK: 'FLAG_RELOGIO_NOK',
78
 
    FLAG_SEM_MF: 'FLAG_SEM_MF',
79
 
    FLAG_DIA_FECHADO: 'FLAG_DIA_FECHADO',
80
 
    FLAG_DIA_ABERTO: 'FLAG_DIA_ABERTO',
81
 
    FLAG_Z_PENDENTE: 'FLAG_Z_PENDENTE',
82
 
    FLAG_SEM_PAPEL: 'FLAG_SEM_PAPEL',
83
 
    FLAG_MECANISM_NOK: 'FLAG_MECANISM_NOK',
84
 
    FLAG_DOCUMENTO_ABERTO: 'FLAG_DOCUMENTO_ABERTO',
85
 
    FLAG_INSCRICOES_OK: 'FLAG_INSCRICOES_OK',
86
 
    FLAG_CLICHE_OK: 'FLAG_CLICHE_OK',
87
 
    FLAG_EM_LINHA: 'FLAG_EM_LINHA',
88
 
    FLAG_MFD_ESGOTADA: 'FLAG_MFD_ESGOTADA',
89
 
    }
90
 
 
91
 
 
92
 
class Pay2023Constants(BaseDriverConstants):
93
 
    _constants = {
94
 
        UnitType.WEIGHT:      'km',
95
 
        UnitType.LITERS:      'lt',
96
 
        UnitType.METERS:      'm ',
97
 
        UnitType.EMPTY:       '  ',
98
 
        PaymentMethodType.MONEY:         '-2',
99
 
        PaymentMethodType.CHECK:        '2',
100
 
#         PaymentMethodType.MONEY: '-2',
101
 
#         PaymentMethodType.CHECK: '0',
102
 
#         PaymentMethodType.BOLETO: '1',
103
 
#         PaymentMethodType.CREDIT_CARD: '2',
104
 
#         PaymentMethodType.DEBIT_CARD: '3',
105
 
#         PaymentMethodType.FINANCIAL: '4',
106
 
#         PaymentMethodType.GIFT_CERTIFICATE: '5,
107
 
        }
108
 
 
109
 
_RETVAL_TOKEN_RE = re.compile(r"^\s*([^=\s;]+)")
110
 
_RETVAL_QUOTED_VALUE_RE = re.compile(r"^\s*=\s*\"([^\"\\]*(?:\\.[^\"\\]*)*)\"")
111
 
_RETVAL_VALUE_RE = re.compile(r"^\s*=\s*([^\s;]*)")
112
 
_RETVAL_ESCAPE_RE = re.compile(r"\\(.)")
113
 
 
114
 
class Pay2023(SerialBase, BaseChequePrinter):
115
 
    implements(IChequePrinter, ICouponPrinter)
 
36
 
 
37
class Pay2023(FiscNetChequePrinter):
 
38
    log_domain = 'PertoPay2023'
 
39
    supported = True
116
40
 
117
41
    model_name = "Pertopay Fiscal 2023"
118
 
    coupon_printer_charset = "cp850"
119
 
    cheque_printer_charset = "ascii"
120
 
 
121
 
    CHEQUE_CONFIGFILE = 'perto.ini'
122
 
 
123
 
    CMD_PREFIX = '{'
124
 
    CMD_SUFFIX = '}'
125
 
    EOL_DELIMIT = CMD_SUFFIX
126
 
 
127
 
    errors_dict = {
128
 
        7003: OutofPaperError,
129
 
        7004: OutofPaperError,
130
 
        8007: CouponTotalizeError,
131
 
        8011: PaymentAdditionError,
132
 
        8013: CouponTotalizeError,
133
 
        8014: PaymentAdditionError,
134
 
        8017: CloseCouponError,
135
 
        8044: CancelItemError,
136
 
        8045: CancelItemError,
137
 
        8068: PaymentAdditionError,
138
 
        8086: CancelItemError,
139
 
        15009: PendingReduceZ,
140
 
        11002: CommandParametersError,
141
 
        11006: CommandError,
142
 
        11007: InvalidState,
143
 
        15007: PendingReadX,
144
 
        15008: ReadXError,
145
 
        15011: OutofPaperError
146
 
        }
147
 
 
148
 
    def __init__(self, port, consts=None):
149
 
        port.set_options(baudrate=115200, parity=PARITY_EVEN)
150
 
        SerialBase.__init__(self, port)
151
 
        BaseChequePrinter.__init__(self)
152
 
        self._consts = consts or Pay2023Constants
153
 
        self._command_id = 0
154
 
        self._reset()
155
 
 
156
 
    def _reset(self):
157
 
        self._customer_name = ''
158
 
        self._customer_document = ''
159
 
        self._customer_address = ''
160
 
 
161
 
    #
162
 
    # Helper methods
163
 
    #
164
 
    def _parse_return_value(self, text):
165
 
        # Based on cookielib.split_header_words
166
 
        def unmatched(match):
167
 
            start, end = match.span(0)
168
 
            return match.string[:start] + match.string[end:]
169
 
 
170
 
        orig_text = text
171
 
        result = {}
172
 
        while text:
173
 
            m = _RETVAL_TOKEN_RE.search(text)
174
 
            if m:
175
 
                text = unmatched(m)
176
 
                name = m.group(1)
177
 
                m = _RETVAL_QUOTED_VALUE_RE.search(text)
178
 
                if m:  # quoted value
179
 
                    text = unmatched(m)
180
 
                    value = m.group(1)
181
 
                    value = _RETVAL_ESCAPE_RE.sub(r"\1", value)
182
 
                else:
183
 
                    m = _RETVAL_VALUE_RE.search(text)
184
 
                    if m:  # unquoted value
185
 
                        text = unmatched(m)
186
 
                        value = m.group(1)
187
 
                        value = value.rstrip()
188
 
                    else:
189
 
                        # no value, a lone token
190
 
                        value = None
191
 
                result[name] = value
192
 
            else:
193
 
                raise AssertionError
194
 
 
195
 
        return result
196
 
 
197
 
    def _send_command(self, command, **params):
198
 
        # Page 38-39
199
 
        parameters = []
200
 
        for param, value in params.items():
201
 
            if isinstance(value, Decimal):
202
 
                value = ('%.03f' % value).replace('.', ',')
203
 
            elif isinstance(value, basestring):
204
 
                value = '"%s"' % value
205
 
            elif isinstance(value, bool):
206
 
                if value is False:
207
 
                    value = 'f'
208
 
                elif value is True:
209
 
                    value = 't'
210
 
            elif isinstance(value, datetime.date):
211
 
                value = value.strftime('#%d/%m/%y#')
212
 
 
213
 
            parameters.append('%s=%s' % (param, value))
214
 
 
215
 
        reply = self.writeline("%d;%s;%s;" % (self._command_id,
216
 
                                              command,
217
 
                                              ' '.join(parameters)))
218
 
        if reply[0] != '{':
219
 
            # This happened once after the first command issued after
220
 
            # the power returned, it should probably be handled gracefully
221
 
            raise AssertionError(repr(reply))
222
 
 
223
 
        # Page 39
224
 
        sections = reply[1:].split(';')
225
 
        if len(sections) != 4:
226
 
            raise AssertionError
227
 
 
228
 
        retdict = self._parse_return_value(sections[2])
229
 
        errorcode = int(sections[1])
230
 
        if errorcode != 0:
231
 
            errorname = retdict['NomeErro']
232
 
            errordesc = retdict['Circunstancia']
233
 
            try:
234
 
                exception = Pay2023.errors_dict[errorcode]
235
 
            except KeyError:
236
 
                raise DriverError(errordesc, errorcode)
237
 
            raise exception(errordesc, errorcode)
238
 
 
239
 
        return retdict
240
 
 
241
 
    def _read_register(self, name, regtype):
242
 
        if regtype == int:
243
 
            cmd = 'LeInteiro'
244
 
            argname = 'NomeInteiro'
245
 
            retname = 'ValorInteiro'
246
 
        elif regtype == Decimal:
247
 
            cmd = 'LeMoeda'
248
 
            argname = 'NomeDadoMonetario'
249
 
            retname = 'ValorMoeda'
250
 
        elif regtype == datetime.date:
251
 
            cmd = 'LeData'
252
 
            argname = 'NomeData'
253
 
            retname = 'ValorData'
254
 
        elif regtype == str:
255
 
            cmd = 'LeTexto'
256
 
            argname = 'NomeTexto'
257
 
            retname = 'ValorTexto'
258
 
        else:
259
 
            raise AssertionError
260
 
 
261
 
        retdict = self._send_command(cmd, **dict([(argname, name)]))
262
 
        assert len(retdict) == 1
263
 
        assert retname in retdict
264
 
        retval = retdict[retname]
265
 
        if regtype == int:
266
 
            return int(retval)
267
 
        elif regtype == Decimal:
268
 
            retval = retval.replace('.', '')
269
 
            retval = retval.replace(',', '.')
270
 
            return Decimal(retval)
271
 
        elif regtype == datetime.date:
272
 
            # This happens the first time we send a ReducaoZ after
273
 
            # opening the printer and removing the jumper.
274
 
            if retval == '#00/00/0000#':
275
 
                return datetime.date.today()
276
 
            else:
277
 
                # "29/03/2007" -> datetime.date(2007, 3, 29)
278
 
                d, m, y = map(int, retval[1:-1].split('/'))
279
 
                return datetime.date(y, m, d)
280
 
        elif regtype == str:
281
 
            # '"string"' -> 'string'
282
 
            return retval[1:-1]
283
 
        else:
284
 
            raise AssertionError
285
 
 
286
 
    def _get_status(self):
287
 
        return self._read_register('Indicadores', int)
288
 
 
289
 
    def _get_last_item_id(self):
290
 
        return self._read_register('ContadorDocUltimoItemVendido', int)
291
 
 
292
 
    def _get_coupon_number(self):
293
 
        return self._read_register('COO', int)
294
 
 
295
 
    def _get_coupon_total_value(self):
296
 
        return self._read_register('TotalDocLiquido', Decimal)
297
 
 
298
 
    def _get_coupon_remainder_value(self):
299
 
        value = self._read_register('TotalDocValorPago', Decimal)
300
 
        result = self._get_coupon_total_value() - value
301
 
        if result < 0.0:
302
 
            result = 0.0
303
 
        return result
304
 
 
305
 
    # This how the printer needs to be configured.
306
 
    def _define_tax_name(self, code, name):
307
 
        try:
308
 
            retdict = self._send_command(
309
 
                'LeNaoFiscal', CodNaoFiscal=code)
310
 
        except DriverError, e:
311
 
            if e.code != 8057: # Not configured
312
 
                raise
313
 
        else:
314
 
            for retname in ['NomeNaoFiscal', 'DescricaoNaoFiscal']:
315
 
                configured_name = retdict[retname]
316
 
                if configured_name  != name:
317
 
                    raise DriverError(
318
 
                        "The name of the tax code %d is set to %r, "
319
 
                        "but it needs to be configured as %r" % (
320
 
                        code, configured_name, name))
321
 
 
322
 
        try:
323
 
            self._send_command(
324
 
                'DefineNaoFiscal', CodNaoFiscal=code, DescricaoNaoFiscal=name,
325
 
                NomeNaoFiscal=name, TipoNaoFiscal=False)
326
 
        except DriverError, e:
327
 
            if e.code != 8036:
328
 
                raise
329
 
 
330
 
    def _delete_tax_name(self, code):
331
 
        try:
332
 
            self._send_command(
333
 
                'ExcluiNaoFiscal', CodNaoFiscal=code)
334
 
        except DriverError, e:
335
 
            if e.code != 8057: # Not configured
336
 
                raise
337
 
 
338
 
    def _define_payment_method(self, code, name):
339
 
        try:
340
 
            retdict = self._send_command(
341
 
                'LeMeioPagamento', CodMeioPagamentoProgram=code)
342
 
        except DriverError, e:
343
 
            if e.code != 8014: # Not configured
344
 
                raise
345
 
        else:
346
 
            configure = False
347
 
            for retname in ['NomeMeioPagamento', 'DescricaoMeioPagamento']:
348
 
                configured_name = retdict[retname]
349
 
                if configured_name  != name:
350
 
                    configure = True
351
 
 
352
 
            if not configure:
353
 
                return
354
 
 
355
 
        try:
356
 
            self._send_command(
357
 
                'DefineMeioPagamento',
358
 
                CodMeioPagamentoProgram=code, DescricaoMeioPagamento=name,
359
 
                NomeMeioPagamento=name, PermiteVinculado=False)
360
 
        except DriverError, e:
361
 
            raise
362
 
 
363
 
    def _delete_payment_method(self, code):
364
 
        try:
365
 
            self._send_command(
366
 
                'ExcluiMeioPagamento', CodMeioPagamentoProgram=code)
367
 
        except DriverError, e:
368
 
            if e.code != 8014: # Not configured
369
 
                raise
370
 
 
371
 
    def _define_tax_code(self, code, value, service=False):
372
 
        try:
373
 
            retdict = self._send_command(
374
 
                'LeAliquota', CodAliquotaProgramavel=code)
375
 
        except DriverError, e:
376
 
            if e.code != 8005: # Not configured
377
 
                raise
378
 
        else:
379
 
            configure = False
380
 
            for retname in ['PercentualAliquota']:
381
 
                configured_name = retdict[retname]
382
 
                if configured_name != value:
383
 
                    configure = True
384
 
 
385
 
            if not configure:
386
 
                return
387
 
 
388
 
        try:
389
 
            self._send_command(
390
 
                'DefineAliquota',
391
 
                CodAliquotaProgramavel=code,
392
 
                DescricaoAliquota='%2.2f%%' % value ,
393
 
                PercentualAliquota=value,
394
 
                AliquotaICMS=not service)
395
 
        except DriverError, e:
396
 
            raise
397
 
 
398
 
    def _delete_tax_code(self, code):
399
 
        try:
400
 
            self._send_command(
401
 
                'ExcluiAliquota', CodAliquotaProgramavel=code)
402
 
        except DriverError, e:
403
 
            if e.code != 8005: # Not configured
404
 
                raise
405
 
 
406
 
    def _get_taxes(self):
407
 
        taxes = [
408
 
            ('I', self._read_register('TotalDiaIsencaoICMS', Decimal)),
409
 
            ('F', self._read_register('TotalDiaSubstituicaoTributariaICMS',
410
 
                                      Decimal)),
411
 
            ('N', self._read_register('TotalDiaNaoTributadoICMS', Decimal)),
412
 
            ('DESC',
413
 
             self._read_register('TotalDiaDescontos', Decimal)),
414
 
            ('CANC',
415
 
             self._read_register('TotalDiaCancelamentosICMS', Decimal) +
416
 
             self._read_register('TotalDiaCancelamentosISSQN', Decimal)),
417
 
            ('ISS',
418
 
             self._read_register('TotalDiaISSQN', Decimal)),
419
 
            ]
420
 
 
421
 
        for reg in range(16):
422
 
            value = self._read_register('TotalDiaValorAliquota[%d]' % (
423
 
                reg,), Decimal)
424
 
            if value:
425
 
                retdict = self._send_command(
426
 
                    'LeAliquota', CodAliquotaProgramavel=reg)
427
 
                # The service taxes are already added in the 'ISS' tax
428
 
                # Skip non-ICMS taxes here.
429
 
                if retdict['AliquotaICMS'] == 'N':
430
 
                    continue
431
 
                desc = retdict['PercentualAliquota'].replace(',', '')
432
 
                taxes.append(('%04d' % int(desc), value))
433
 
        return taxes
434
 
 
435
 
    def setup(self):
436
 
        self._define_tax_name(0, "Suprimento".encode('cp850'))
437
 
        self._define_tax_name(1, "Sangria".encode('cp850'))
438
 
        for code in range(2, 15):
439
 
            self._delete_tax_name(code)
440
 
 
441
 
        self._define_payment_method(0, u'Cheque'.encode('cp850'))
442
 
        self._define_payment_method(1, u'Boleto'.encode('cp850'))
443
 
        self._define_payment_method(2, u'Cartão credito'.encode('cp850'))
444
 
        self._define_payment_method(3, u'Cartão debito'.encode('cp850'))
445
 
        self._define_payment_method(4, u'Financeira'.encode('cp850'))
446
 
        self._define_payment_method(5, u'Vale compra'.encode('cp850'))
447
 
        for code in range(6, 15):
448
 
            self._delete_payment_method(code)
449
 
 
450
 
        self._define_tax_code(0, Decimal("17.00"))
451
 
        self._define_tax_code(1, Decimal("12.00"))
452
 
        self._define_tax_code(2, Decimal("25.00"))
453
 
        self._define_tax_code(3, Decimal("8.00"))
454
 
        self._define_tax_code(4, Decimal("5.00"))
455
 
        self._define_tax_code(5, Decimal("3.00"), service=True)
456
 
        for code in range(6, 16):
457
 
            self._delete_tax_code(code)
458
 
 
459
 
    def print_status(self):
460
 
        status = self._get_status()
461
 
        print 'Flags'
462
 
        for flag in reversed(_status_flags):
463
 
            if status & flag:
464
 
                print flag, _flagnames[flag]
465
 
 
466
 
        print 'non-fiscal registers'
467
 
        for i in range(15):
468
 
            try:
469
 
                print self._send_command(
470
 
                    'LeNaoFiscal', CodNaoFiscal=i)
471
 
            except DriverError, e:
472
 
                if e.code != 8057:
473
 
                    raise
474
 
 
475
 
    #
476
 
    # ICouponPrinter implementation
477
 
    #
478
 
 
479
 
    def coupon_identify_customer(self, customer, address, document):
480
 
        self._customer_name = customer
481
 
        self._customer_document = document
482
 
        self._customer_address = address
483
 
 
484
 
    def coupon_is_customer_identified(self):
485
 
        return len(self._customer_document) > 0
486
 
 
487
 
    def coupon_open(self):
488
 
        status = self._get_status()
489
 
        if status & FLAG_DOCUMENTO_ABERTO:
490
 
            raise CouponOpenError(_("Coupon already opened."))
491
 
 
492
 
        customer = self._customer_name
493
 
        document = self._customer_document
494
 
        address = self._customer_address
495
 
        self._send_command('AbreCupomFiscal',
496
 
                           EnderecoConsumidor=address[:80],
497
 
                           IdConsumidor=document[:29],
498
 
                           NomeConsumidor=customer[:30])
499
 
 
500
 
    def coupon_add_item(self, code, description, price, taxcode,
501
 
                        quantity=Decimal("1.0"), unit=UnitType.EMPTY,
502
 
                        discount=Decimal("0.0"), surcharge=Decimal("0.0"),
503
 
                        unit_desc=""):
504
 
        status = self._get_status()
505
 
        if not status & FLAG_DOCUMENTO_ABERTO:
506
 
            raise CouponNotOpenError
507
 
 
508
 
        if unit == UnitType.CUSTOM:
509
 
            unit = unit_desc
510
 
        else:
511
 
            unit = self._consts.get_value(unit)
512
 
 
513
 
        taxcode = ord(taxcode) - 128
514
 
        self._send_command('VendeItem',
515
 
                           CodAliquota=taxcode,
516
 
                           CodProduto=code[:48],
517
 
                           NomeProduto=description[:200],
518
 
                           Unidade=unit,
519
 
                           PrecoUnitario=price,
520
 
                           Quantidade=quantity)
521
 
        return self._get_last_item_id()
522
 
 
523
 
    def coupon_cancel_item(self, item_id):
524
 
        self._send_command('CancelaItemFiscal', NumItem=item_id)
525
 
 
526
 
    def coupon_cancel(self):
527
 
        self._send_command('CancelaCupom')
528
 
 
529
 
    def cancel_last_coupon(self):
530
 
        """Cancel the last non fiscal coupon or the last sale."""
531
 
        self._send_command('CancelaCupom')
532
 
 
533
 
    def coupon_totalize(self, discount=Decimal("0.0"),
534
 
                        surcharge=Decimal("0.0"), taxcode=TaxType.NONE):
535
 
        # The FISCnet protocol (the protocol used in this printer model)
536
 
        # doesn't have a command to totalize the coupon, so we just get
537
 
        # the discount/surcharge values and applied to the coupon.
538
 
        value = discount and (discount * -1) or surcharge
539
 
        if value:
540
 
            self._send_command('AcresceSubtotal',
541
 
                               Cancelar=False,
542
 
                               ValorPercentual=value)
543
 
        return self._get_coupon_total_value()
544
 
 
545
 
    def coupon_add_payment(self, payment_method, value, description=u"",
546
 
                           custom_pm=''):
547
 
        if not custom_pm:
548
 
            pm = int(self._consts.get_value(payment_method))
549
 
        else:
550
 
            pm = custom_pm
551
 
        self._send_command('PagaCupom',
552
 
                           CodMeioPagamento=pm, Valor=value,
553
 
                           TextoAdicional=description[:80])
554
 
        return self._get_coupon_remainder_value()
555
 
 
556
 
    def coupon_close(self, message=''):
557
 
        self._send_command('EncerraDocumento',
558
 
                           TextoPromocional=message[:492])
559
 
        self._reset()
560
 
        return self._get_coupon_number()
561
 
 
562
 
    def summarize(self):
563
 
        self._send_command('EmiteLeituraX')
564
 
 
565
 
    def close_till(self, previous_day=False):
566
 
        status = self._get_status()
567
 
        if status & FLAG_DOCUMENTO_ABERTO:
568
 
            self.coupon_cancel()
569
 
 
570
 
        data = Settable(
571
 
            opening_date=self._read_register('DataAbertura', datetime.date),
572
 
            serial=self._read_register('NumeroSerieECF', str),
573
 
            serial_id=self._read_register('ECF', int),
574
 
            coupon_start=self._read_register('COOInicioDia', int),
575
 
            coupon_end=self._read_register('COO', int),
576
 
            cro=self._read_register('CRO', int),
577
 
            crz=self._read_register('CRZ', int),
578
 
            period_total=self._read_register('TotalDiaVendaBruta', Decimal),
579
 
            total=self._read_register('GT', Decimal),
580
 
            taxes=self._get_taxes())
581
 
 
582
 
        self._send_command('EmiteReducaoZ')
583
 
 
584
 
        return data
585
 
 
586
 
    def till_add_cash(self, value):
587
 
        status = self._get_status()
588
 
        if status & FLAG_DOCUMENTO_ABERTO:
589
 
            self.coupon_cancel()
590
 
        self._send_command('AbreCupomNaoFiscal')
591
 
        self._send_command('EmiteItemNaoFiscal',
592
 
                           NomeNaoFiscal="Suprimento",
593
 
                           Valor=value)
594
 
        self._send_command('EncerraDocumento')
595
 
 
596
 
    def till_remove_cash(self, value):
597
 
        status = self._get_status()
598
 
        if status & FLAG_DOCUMENTO_ABERTO:
599
 
            self.coupon_cancel()
600
 
        self._send_command('AbreCupomNaoFiscal')
601
 
        self._send_command('EmiteItemNaoFiscal',
602
 
                           NomeNaoFiscal="Sangria",
603
 
                           Valor=value)
604
 
        self._send_command('EncerraDocumento')
605
 
 
606
 
    def till_read_memory(self, start=None, end=None):
607
 
        try:
608
 
            self._send_command('EmiteLeituraMF',
609
 
                               LeituraSimplificada=True,
610
 
                               DataInicial=start,
611
 
                               DataFinal=end)
612
 
        except DriverError, e:
613
 
            if e.code == 8089:
614
 
                return
615
 
 
616
 
    def till_read_memory_by_reductions(self, start=None, end=None):
617
 
        self._send_command('EmiteLeituraMF',
618
 
                           LeituraSimplificada=True,
619
 
                           ReducaoInicial=start,
620
 
                           ReducaoFinal=end)
621
 
 
622
 
    #
623
 
    # IChequePrinter implementation
624
 
    #
625
 
 
626
 
    def print_cheque(self, bank, value, thirdparty, city, date=None):
627
 
        if date is None:
628
 
            data = datetime.datetime.now()
629
 
        if not isinstance(bank, BankConfiguration):
630
 
            raise TypeError("bank parameter must be BankConfiguration instance")
631
 
 
632
 
        data = dict(HPosAno=bank.get_x_coordinate("year"),
633
 
                    HPosCidade=bank.get_x_coordinate("city"),
634
 
                    HPosDia=bank.get_x_coordinate("day"),
635
 
                    HPosExtensoLinha1=bank.get_x_coordinate("legal_amount"),
636
 
                    HPosExtensoLinha2=bank.get_x_coordinate("legal_amount2"),
637
 
                    HPosFavorecido=bank.get_x_coordinate("thirdparty"),
638
 
                    HPosMes=bank.get_x_coordinate("month"),
639
 
                    HPosValor=bank.get_x_coordinate("value"),
640
 
                    VPosCidade=bank.get_y_coordinate("city"),
641
 
                    VPosExtensoLinha1=bank.get_y_coordinate("legal_amount"),
642
 
                    VPosExtensoLinha2=bank.get_y_coordinate("legal_amount2"),
643
 
                    VPosFavorecido=bank.get_y_coordinate("thirdparty"),
644
 
                    VPosValor=bank.get_y_coordinate("value"))
645
 
 
646
 
        self._send_command('ImprimeCheque', Cidade=city[:27],
647
 
                           Data=date.strftime("#%d/%m/%Y#"),
648
 
                           Favorecido=thirdparty[:45],
649
 
                           Valor=value, **data)
650
 
 
651
 
    def get_capabilities(self):
652
 
        return dict(item_code=Capability(max_len=48),
653
 
                    item_id=Capability(max_size=32767),
654
 
                    items_quantity=Capability(digits=14, decimals=4),
655
 
                    item_price=Capability(digits=14, decimals=4),
656
 
                    item_description=Capability(max_len=200),
657
 
                    payment_value=Capability(digits=14, decimals=4),
658
 
                    promotional_message=Capability(max_len=492),
659
 
                    payment_description=Capability(max_len=80),
660
 
                    customer_name=Capability(max_len=30),
661
 
                    customer_id=Capability(max_len=29),
662
 
                    customer_address=Capability(max_len=80),
663
 
                    cheque_thirdparty=Capability(max_len=45),
664
 
                    cheque_value=Capability(digits=14, decimals=4),
665
 
                    cheque_city=Capability(max_len=27))
666
 
 
667
 
    def get_constants(self):
668
 
        return self._consts
669
 
 
670
 
    def query_status(self):
671
 
        return '{0;LeInteiro;NomeInteiro="Indicadores";}'
672
 
 
673
 
    def status_reply_complete(self, reply):
674
 
        return '}' in reply
675
 
 
676
 
    def get_serial(self):
677
 
        return self._read_register('NumeroSerieECF', str)
678
 
 
679
 
    def get_tax_constants(self):
680
 
        constants = []
681
 
 
682
 
        for reg in range(16):
683
 
            try:
684
 
                retdict = self._send_command(
685
 
                    'LeAliquota', CodAliquotaProgramavel=reg)
686
 
            except DriverError, e:
687
 
                if e.code == 8005: # Aliquota nao carregada
688
 
                    continue
689
 
                raise
690
 
            print retdict
691
 
            # The service taxes are already added in the 'ISS' tax
692
 
            # Skip non-ICMS taxes here.
693
 
            if retdict['AliquotaICMS'] == 'Y':
694
 
                tax_type = TaxType.CUSTOM
695
 
            else:
696
 
                tax_type = TaxType.SERVICE
697
 
 
698
 
            value = Decimal(retdict['PercentualAliquota'].replace(',', '.'))
699
 
            device_value = int(retdict['CodAliquotaProgramavel'])
700
 
            constants.append((tax_type,
701
 
                              chr(128 + device_value),
702
 
                              value))
703
 
 
704
 
        # These are signed integers, we're storing them
705
 
        # as strings and then subtract by 127
706
 
        # Page 10
707
 
        constants.extend([
708
 
            (TaxType.SUBSTITUTION, '\x7e', None), # -2
709
 
            (TaxType.EXEMPTION,    '\x7d', None), # -3
710
 
            (TaxType.NONE,         '\x7c', None), # -4
711
 
            ])
712
 
 
713
 
        return constants
 
42