class TaxType(IntEnum):
    """
    Enum for taxes
    """
    (ICMS,
     SUBSTITUTION,
     EXEMPTION,
     NONE,
     SERVICE,
     CUSTOM) = range(40, 46) class UnitType(IntEnum):
    """
    Enum for units
    """
    (WEIGHT,
     METERS,
     LITERS,
     EMPTY,
     CUSTOM) = range(20, 25) class DriverError(Exception):
    "Base exception for all printer errors"

    def __init__(self, error='', code=-1):
        if code != -1:
            error = '%d: %s' % (code, error)
        Exception.__init__(self, error)
        self.code = code class OutofPaperError(DriverError):
    "No paper left" class PrinterError(Exception):
    "General printer errors" class CommandError(DriverError):
    "Command sent to printer is wrong." class CouponOpenError(DriverError):
    "Coupon already is open." class HardwareFailure(DriverError):
    "Unknown hardware failure" class PrinterOfflineError(DriverError):
    "Printer is offline" class PaymentAdditionError(DriverError):
    "Error while adding a payment." class ItemAdditionError(DriverError):
    "Error while adding an item." class CancelItemError(DriverError):
    "Error while cancelling coupon item." class CouponTotalizeError(DriverError):
    "Error while totalizing a coupon." class CouponNotOpenError(DriverError):
    "Coupon is not open." class ICouponPrinter(IDevice):
    """ Describes coupon related tasks for a printer.

    Workflow::

                                    --<--                     --<--
                                   |     |                   |     |
    [identify_customer] -> open -> add_item -> totalize -> add_payment -> close
    """

    coupon_printer_charset = Attribute("The charset name which the "
                                       "coupon printer uses.")

    supports_duplicate_receipt = Attribute(
        "Wheater or not the printer can print a "
        "duplicate payment receipt")

    identify_customer_at_end = Attribute(
        "True if the customer is identified at the"
        "end of the sale. False if at the beginning")

    #
    # Common API
    #

    def setup():
        """Setup printer for use.

        This method should do any logic required for the correct functioning of
        the printer, like checking if it uses 2 or 3 decimal places for product
        prices and quantity, and using this properties when necessary.
        """

    def coupon_identify_customer(customer, address, document):
        """ Identify the customer.  This method doesn't have mandatory
        execution (you can identify the customer only if you like), but when
        executed it must be called before calling any method.

        @param customer:
        @type customer:   str

        @param address:
        @type address:    str

        @param document:
        @type document:   str
        """

    def coupon_is_customer_identified():
        """ Returns True, if the customer have already been identified,
        False otherwise.
        """

    def has_open_coupon():
        """Returns True if the printer currently has an open coupon"""

    def coupon_open():
        """ This needs to be called before anything else (except
        identify_customer())
        """

    def coupon_add_item(code, description, price, taxcode,
                        quantity=Decimal("1.0"), unit=UnitType.EMPTY,
                        discount=Decimal("0.0"),
                        surcharge=Decimal("0.0"), unit_desc=""):
        """ Adds an item to the coupon.

        @param code:      item code identifier
        @type  code:      str

        @param description:  description of product
        @type  desription: str

        @param price:     price
        @type  price:     Decimal

        @param taxcode:   constant to descrive the tax
        @type  taxcode:   str

        @param quantity:  quantity
        @type  quantity:  Decimal

        @param unit:      constant to describe the unit
        @type unit:       integer constant one of: UnitType.LITERS, UnitType.EMPTY,
                          UnitType.METERS, UnitType.WEIGHT, UnitType.CUSTOM.

        @param discount:  discount in %
        @type  discount:  Decimal between 0-100

        @param surcharge: surcharge in %
        @type  surcharge: Decimal between 0-100

        @param unit_desc: A 2-byte string representing the unit that applies to
                          the product.
        @type unit_desc:  str

        @rtype:           Decimal
        @returns:        identifier of added item
        """

    def coupon_cancel_item(item_id):
        """ Cancels an item, item_id must be a value returned by
        coupon_add_item

        @param item_id:   the item id
        """

    def coupon_cancel():
        """
        Cancels the currently open coupon or the last closed open.
        You cannot close a previous coupon if you already created
        a new one.
        """

    def cancel_last_coupon():
        """
        Cancel the last closed non-fiscal coupon or the last sale.
        """

    def coupon_totalize(discount=Decimal(0), surcharge=Decimal(0),
                        taxcode=TaxType.NONE):
        """ Closes the coupon applies addition a discount or surcharge and tax.
        This can only be called when the coupon is open, has items added and
        payments added.

        @param discount:  discount in value
        @type discount:   Value must be greater or equal than zero, and less
                          than coupon total value

        @param surcharge: surcharge in value
        @type  surcharge: Value must be greater or equal than zero

        @param taxcode:   constant to descrive the tax
        @type  taxcode:   integer constant one of: TaxType.NONE, TaxType.SUBSTITUTION,
                          TaxType.EXEMPTION

        @rtype:           Decimal
        @returns          the coupon total value
        """

    def coupon_add_payment(payment_method, value, description=u""):
        """
        Add payment method to coupon.
        @param payment_method: The payment method.
        @type payment_method:  a device specific value representing the payment value

        @param value:     The payment value
        @type value:      Decimal

        @param description: A simple description of the payment method to be
                            appended to the coupon.
        @type value:      unicode

        @rtype:           Decimal
        @returns:         the total remaining amount
        """

    def coupon_close(message=''):
        """ It needs to be possible to open new coupons after this is called.
        You must call coupon_totalize before calling this method.

        @param message:   promotional message
        @type message:    str

        @rtype:           int
        @returns:         identifier of the coupon.
        """

    #
    # Base admin operations
    #

    def summarize():
        """ Prints a summary of all sales of the day. In Brazil this is
        'read X' operation.
        """

    def open_till():
        """ Open the till so fiscal documents can be emitted.

        Some printers simply emit a read X document, but some printers may
        require an special command
        """

    def has_pending_reduce():
        """ Verify is has pending 'reduce z'
        """

    def close_till(previous_day=False):
        """ Close the till for the day, no other actions can be done after
        this is called. In Brazil this is 'reduce Z' operation

        @param previous_day: if this is to close a till opened previously
        """

    def till_add_cash(value):
        """ Add an till complement. This is called 'suprimento de caixa' on
        Brazil

        @param value:     The value added
        @type value:      Decimal
        """

    def till_remove_cash(value):
        """ Retire payments from the till. This is called 'sangria' on Brazil

        @param value:     The value to remove
        @type value:      Decimal
        """

    def till_read_memory(start, end):
        """
        Reads the fiscal memory, from the date start to the date end

        @param start: start date
        @type start: datetime.date
        @param end: end date
        @type end: datetime.date
        """

    def till_read_memory_by_reductions(start, end):
        """
        Reads the fiscal memory, from the start reductions to the end
        reductions

        @param start: start reductions
        @type start: int
        @param end: end reductions
        @type end: int
        """

    def get_payment_receipt_identifier(method_name):
        """If the printer requires an identifier to open a payment receipt,
        this method should return that identifier.

        @param method_name: get receipt for specific method name.
        """

    def payment_receipt_open(identifier, coo, method, value):
        """Opens a non-fiscal bound receipt for a payment

        @param identifier: a char (A-P) representing the receipt to use.
                           The receipt must already be configured at the printer.
        @param method: another char (A-P) for the payment method. Also, this must be
                       configured at the printer. The payment method must be bindable
        @param coo: coo for the coupon containing the payment
        @param value: the value of the payment
        """

    def payment_receipt_print(text):
        """Prints the payment receipt text

        @param text: text to be printed
        """

    def payment_receipt_close():
        """Closes previouly opened payment receipt
        """

    #
    # Getting printer status
    #

    def get_capabilities():
        """ Returns a capabilities dictionary, where the keys are the strings
        below and its values are Capability instances

        * item_code           (str)
        * item_id             (int)
        * items_quantity      (float)
        * item_price          (float)
        * item_description    (str)
        * payment_value       (float)
        * payment_description (str)
        * promotional_message (str)
        * customer_name       (str)
        * customer_id         (str)
        * customer_address    (str)
        * add_cash_value      (float)
        * remove_cash_value   (float)
        """

    def get_constants():
        """ Returns the object that implements IDriverConstants where the printer
        is going to get constant values from.
        """

    def get_payment_constants():
        """"""

    def get_serial():
        """Returns the serial number for the printer"""

    def get_sintegra():
        """
        Gets an object that implements ISintegraData or None.
        """

    def get_firmware_version():
        """Firmware version of the printer
        """

    def get_ccf():
        """Fiscal Coupon Counter

        @returns: the last document ccf
        @rtype: integer
        """

    def get_coo():
        """Operation Order Counter

        @returns: the last document coo
        @rtype: integer
        """

    def get_gnf():
        """Nonfiscal Operation General Counter

        @returns: gnf
        @rtype: integer
        """

    def get_crz():
        """Z Reduction Counter

        @returns: the last document crz
        @rtype: integer
        """ class BaseDriverConstants:

    # Must be defined on subclasses
    _constants = None

    @classmethod
    def get_items(cls):
        return list(cls._constants.keys())

    @classmethod
    def get_value(cls, identifier, default=_NoDefault):
        try:
            return cls._constants[identifier]
        except KeyError:
            if default is not _NoDefault:
                return default
            raise ValueError("The constant identifier %r "
                             "isn't valid", identifier) class Capability:
    """ This class is used to represent a driver capability, offering methods
    to validate a value with base in the capability limits.
    """

    def __init__(self, min_len: Optional[int]=None, max_len: Optional[int]=None,
                 max_size: Optional[Real]=None, min_size: Optional[Real]=None,
                 digits: Optional[int]=None, decimals: Optional[Real]=None):
        """ Creates a new driver capability.  A driver capability can be
        represented basically by the max length of a string, the max digits
        number of a value or its minimum/maximum size.  With an instance of
        Capability you can check if a value is acceptable by the driver
        through the check_value method.  The Capability arguments are:

        @param min_len:    The minimum length of a string
        @type min_len:     int
        @param max_len:    The max length of a string
        @type max_len:     int
        @param max_size    The maximum size for a value
        @type max_size:    number
        @param min_size:   The minimum size for a value
        @type min_size:    number
        @param digits:     The number of digits that a number can have
        @type digits:      int
        @param decimals:   If the max value for the capability is a float,
                           this parameter specifies the max precision
                           that the number can have.
        @type decimals:    number

        Note that 'max_len' can't be used together with 'min_size', 'max_size'
        and 'digits', in the same way that 'max_size' and 'min_size' can't be
        used with 'digits'.  The values defined for these parameters are used
        also to verify the value type in the 'check_value' method.
        """

        if max_len is not None and (max_size is not None
                                    and min_size is not None
                                    and digits is not None
                                    and decimals):
            raise ValueError("max_len cannot be used together with max_size, "
                             "min_size, digits or decimals")
        if digits is not None:
            if max_size is not None:
                raise ValueError("digits can't be used with max_size")
            if decimals:
                decimal_part = 1 - (1 / 10.0 ** decimals)
            else:
                decimal_part = 0
            self.max_size = ((10.0 ** digits) - 1) + decimal_part

        self.min_len = min_len
        self.max_len = max_len
        self.min_size = min_size or 0
        self.max_size = max_size
        self.digits = digits
        self.decimals = decimals

    def check_value(self, value):
        if self.max_len:
            if not isinstance(value, str):
                raise CapabilityError("the value must be a string")
            if len(value) > (self.max_len or float('inf')):
                raise CapabilityError("the value can't be greater than %d "
                                      "characters" % self.max_len)
            elif len(value) < (self.min_len or float('-inf')):
                raise CapabilityError("the value can't be less than %d "
                                      "characters" % self.min_len)
            return
        elif not (self.max_size and self.min_size):
            return

        if not isinstance(value, (float, int)):
            raise CapabilityError("the value must be float, integer or long")

        if value > (self.max_size or float('inf')):
            raise CapabilityError("the value can't be greater than %r"
                                  % self.max_size)
        elif value < (self.min_size or float('-inf')):
            raise CapabilityError("the value can't be less than %r"
                                  % self.min_size) class FiscalPrinter(BasePrinter):
    def __init__(self, brand=None, model=None, device=None, config_file=None,
                 *args, **kwargs):
    def setup_complete(self):
    def get_capabilities(self):
    def _format_text(self, text):
    def setup(self):
    def identify_customer(self, customer_name: str, customer_address: str, customer_id: str):
    def coupon_is_customer_identified(self):
    def has_open_coupon(self):
    def open(self):
    def add_item(self, item_code: str, item_description: str, item_price: Real, taxcode: TaxType,
                 items_quantity=Decimal("1.0"), unit=UnitType.EMPTY,
                 discount=Decimal("0.0"), surcharge=Decimal("0.0"),
                 unit_desc=""):
    def totalize(self, discount=Decimal(0), surcharge=Decimal(0),
                 taxcode=TaxType.NONE):
    def add_payment(self, payment_method: str, payment_value: Decimal, description=''):
    def cancel(self):
    def cancel_last_coupon(self):
    def cancel_item(self, item_id: int):
    def close(self, promotional_message=''):
    def summarize(self):
    def has_pending_reduce(self):
    def open_till(self):
    def close_till(self, previous_day=False):
    def till_add_cash(self, add_cash_value: Decimal):
    def till_remove_cash(self, remove_cash_value: Decimal):
    def till_read_memory(self, start: datetime.date, end: datetime.date):
    def till_read_memory_to_serial(self, start: datetime.date, end: datetime.date):
    def till_read_memory_by_reductions(self, start: int, end: int):
    def gerencial_report_open(self):
    def gerencial_report_print(self, text):
    def gerencial_report_close(self):
    def payment_receipt_open(self, identifier, coo, method, value):
    def payment_receipt_print(self, text):
    def payment_receipt_close(self):
    def payment_receipt_print_duplicate(self):
    def get_serial(self):
    def query_status(self):
    def status_reply_complete(self, reply):
    def get_tax_constants(self):
    def get_payment_constants(self):
    def get_payment_receipt_identifier(self, method):
    def get_ccf(self):
    def get_coo(self):
    def get_gnf(self):
    def get_crz(self):
    def get_sintegra(self):
    def supports_duplicate_receipt(self):
    def identify_customer_at_end(self): class SerialBase(object):

    # All commands will have this prefixed
    CMD_PREFIX = '\x1b'
    CMD_SUFFIX = ''

    # used by readline()
    EOL_DELIMIT = '\r'

    # Most serial printers allow connecting a cash drawer to them. You can then
    # open the drawer, and also check its status. Some models, for instance,
    # the Radiant drawers, use inverted logic to describe whether they are
    # open, specified by this attribute, settable via BaseDevice config.
    inverted_drawer = False

    def __init__(self, port):
        self.set_port(port)

    def set_port(self, port):
        self._port = port

    def get_port(self):
        return self._port

    def fileno(self):
        return self._port.fileno()

    def writeline(self, data):
        self.write(self.CMD_PREFIX + data + self.CMD_SUFFIX)
        return self.readline()

    def write(self, data):
        # pyserial is expecting bytes but we work with str in stoqdrivers
        data = str2bytes(data)
        log.debug(">>> %r (%d bytes)" % (data, len(data)))
        self._port.write(data)

    def read(self, n_bytes):
        # stoqdrivers is expecting str but pyserial will reply with bytes
        data = self._port.read(n_bytes)
        return bytes2str(data)

    def readline(self):
        out = ''
        a = 0
        retries = 10
        while True:
            if a > retries:
                raise DriverError(_("Timeout communicating with fiscal "
                                    "printer"))

            c = self.read(1)
            if not c:
                a += 1
                print('take %s' % a)
                continue
            a = 0
            if c == self.EOL_DELIMIT:
                log.debug('<<< %r' % out)
                return out
            out += c

    def open(self):
        if not self._port.is_open:
            self._port.open()

    def close(self):
        if self._port.is_open:
            # Flush whaterver is pending to write, since port.close() will close it
            # *imediatally*, losing what was pending to write.
            self._port.flush()
            self._port.close() def stoqdrivers_gettext(message):
    return gettext.dgettext('stoqdrivers', message) def bytes2str(data):
    return ''.join(chr(i) for i in data) def str2bytes(text):
    if isinstance(text, bytes):
        return text
    return bytes(ord(i) for i in text) def encode_text(text, encoding):
    """ Converts the string 'text' to encoding 'encoding' and optionally
    normalizes the string (currently only for ascii)

    @param text:       text to convert
    @type text:        str
    @param encoding:   encoding to use
    @type text:        str
    @returns:          converted text
    """
    if encoding == "ascii":
        text = unicodedata.normalize("NFKD", text)
    # If we use text.encode we will get this sometimes:
    # TypeError: 'abicomp' encoder returned 'str' instead of 'bytes';
    #   use codecs.encode() to encode to arbitrary types
    text = codecs.encode(text, encoding, "ignore")
    # Only do the bellow conversion if the encoding above worked
    if isinstance(text, bytes):
        # Use bytes2str instead of decode or otherwise we would encode
        # the bytes in unicode. This way we will still keep them in the
        # encoding that we want (since SerialBase.write will do the reversal
        # operation, str2bytes) even though we will get some strange
        # characters in the "unicode version" of the string
        text = bytes2str(text)
    return text
import datetime
import logging
import struct
from decimal import Decimal
from zope.interface import implementer
from stoqdrivers.enum import TaxType, UnitType
from stoqdrivers.exceptions import (DriverError, OutofPaperError, PrinterError,
                                    CommandError, CouponOpenError,
                                    HardwareFailure,
                                    PrinterOfflineError, PaymentAdditionError,
                                    ItemAdditionError, CancelItemError,
                                    CouponTotalizeError, CouponNotOpenError)
from stoqdrivers.interfaces import ICouponPrinter
from stoqdrivers.printers.base import BaseDriverConstants
from stoqdrivers.printers.capabilities import Capability
from stoqdrivers.printers.fiscal import SintegraData
from stoqdrivers.serialbase import SerialBase
from stoqdrivers.translation import stoqdrivers_gettext
from stoqdrivers.utils import bytes2str, str2bytes, encode_text
    from stoqdrivers.serialbase import SerialPort
CMD_ADD_VOUCHER = 25
CMD_READ_TAXCODES = 26
CMD_READ_TOTALIZERS = 27
CMD_GET_COUPON_SUBTOTAL = 29
CMD_GET_COUPON_NUMBER = 30
CMD_CANCEL_ITEM = 31
CMD_COUPON_TOTALIZE = 32  # MP 20 Nao permite desconto simultaneo
CMD_COUPON_CLOSE = 34
CMD_READ_REGISTER = 35  # Registradores diferentes
CMD_ADD_ITEM = 63
CMD_PAYMENT_RECEIPT_OPEN = 66  # MP20 suporta somente 3 primeiro parametros
CMD_PAYMENT_RECEIPT_PRINT = 67
CMD_PAYMENT_RECEIPT_CLOSE = 21  # Parece o mesmo, mas com nomes diferentes na mp20 e 25
CMD_GERENCIAL_REPORT_PRINT = 20
CMD_GERENCIAL_REPORT_CLOSE = 21

CMD_PROGRAM_PAYMENT_METHOD = 71
CMD_ADD_PAYMENT = 72

# Be carefull with these commands, They are only available for MP25 (not mp20)
CMD_CANCEL_LAST = 81
CMD_PAYMENT_RECEIPT_PRINT_DUPLICATE = 91

NAK = 21
ACK = 6
STX = 2

RETRIES_BEFORE_TIMEOUT = 40
CHARS_LIMIT = 492
ALLOW_CANCEL_FISCAL_COUPON = 32


# Page 51
class MP25Registers(object):
    TOTAL = 3
    TOTAL_CANCELATIONS = 4
    TOTAL_DISCOUNT = 5
    COO = 6
    GNF = 7
    NUMBER_REDUCTIONS_Z = 9
    CRO = 10
    LAST_ITEM_ID = 12
    NUMBER_TILL = 14
    FISCAL_FLAGS = 17
    EMISSION_DATE = 23
    TRUNC_FLAG = 28
    TOTALIZERS = 29
    PAYMENT_METHODS = 32
    SERIAL = 40
    FIRMWARE = 41
    CCF = 55

    # (size, bcd)
    formats = {
        TOTAL: ('9s', True),
        TOTAL_CANCELATIONS: ('7s', True),
        TOTAL_DISCOUNT: ('7s', True),
        COO: ('3s', True),
        GNF: ('3s', True),
        NUMBER_REDUCTIONS_Z: ('2s', True),
        CRO: ('2s', True),
        LAST_ITEM_ID: ('2s', True),
        NUMBER_TILL: ('2s', True),
        FISCAL_FLAGS: ('1s', False),
        EMISSION_DATE: ('6s', False),
        TRUNC_FLAG: ('1s', False),
        TOTALIZERS: ('2s', False),
        #  1 + (52 * 16) + (52 * 10) + (52 * 10) + (52 * 1)
        #  1 + 832 + 520 + 520 + 52: 1925
        PAYMENT_METHODS: ('b832s520s520s52s', False),
        SERIAL: ('20s', False),
        FIRMWARE: ('3s', True),
        CCF: ('3s', True),
    }


class MP25Constants(BaseDriverConstants):
    _constants = {
        UnitType.WEIGHT: 'Kg',
        UnitType.METERS: 'm ',
        UnitType.LITERS: 'Lt',
        UnitType.EMPTY: '  ',
    }


class MP25Status(object):
    PENDING_REDUCE_Z = 66

    st1_codes = {
        128: (OutofPaperError(_("Printer is out of paper"))),
        # 64: (AlmostOutofPaper(_("Printer almost out of paper"))),
        32: (PrinterError(_("Printer clock error"))),
        16: (PrinterError(_("Printer in error state"))),
        8: (CommandError(_("First data value in CMD is not ESC (1BH)"))),
        4: (CommandError(_("Nonexistent command"))),
        # 2: (CouponOpenError(_("Printer has a coupon currently open"))),
        1: (CommandError(_("Invalid number of parameters")))}

    st2_codes = {
        128: (CommandError(_("Invalid CMD parameter"))),
        64: (HardwareFailure(_("Fiscal memory is full"))),
        32: (HardwareFailure(_("Error in CMOS memory"))),
        16: (PrinterError(_("Given tax is not programmed on the printer"))),
        8: (DriverError(_("No available tax slot"))),
        4: (CancelItemError(_("The item wasn't added in the coupon or can't "
                              "be cancelled"))),

        # 2: (PrinterError(_("Owner data (CGC/IE) not programmed on the printer"))),
        # FIXME: This shouldn't be commented. But it will break the tests.
        # Need to update the tests for all bematech printers
        # 1: (CommandError(_("Command not executed")))
    }

    st3_codes = {
        # 7: (CouponOpenError(_("Coupon already Open"))),
        # 8: (CouponNotOpenError(_("Coupon is closed"))),
        13: (PrinterOfflineError(_("Printer is offline"))),
        16: (DriverError(_("Surcharge or discount greater than coupon total"
                           "value"))),
        17: (DriverError(_("Coupon with no items"))),

        20: (PaymentAdditionError(_("Payment method not recognized"))),