satcfe/base.py
class BibliotecaSAT(object):
    """Configura a localização da biblioteca que efetivamente acessará o
    equipamento SAT. A biblioteca deverá ser uma DLL (*dynamic linked library*,
    em sistemas Microsoft Windows) ou uma *shared library* em sistemas baseados
    no UNIX ou GNU/Linux.

    :param string caminho: Caminho completo para a biblioteca SAT.

    :param integer convencao: Opcional. Indica a convenção de chamada da
        biblioteca, devendo ser uma das constantes definidas em
        :attr:`~satcomum.constantes.CONVENCOES_CHAMADA`. Se não for informado,
        a convenção de chamada será decidida conforme a extensão do nome do
        arquivo, assumindo :attr:`~satcomum.constantes.WINDOWS_STDCALL` para as
        extensões ``.DLL`` ou ``.dll``. Quaisquer outras extensões, assume a
        convenção de chamada :attr:`~satcomum.constantes.STANDARD_C`.

    """

    def __init__(self, caminho, convencao=None):
        self._libsat = None
        self._caminho = caminho
        self._convencao = convencao
        self._carregar()

    def _carregar(self):
        """Carrega (ou recarrega) a biblioteca SAT. Se a convenção de chamada
        ainda não tiver sido definida, será determinada pela extensão do
        arquivo da biblioteca.

        :raises ValueError: Se a convenção de chamada não puder ser determinada
            ou se não for um valor válido.
        """
        if self._convencao is None:
            if self._caminho.endswith(('.DLL', '.dll')):
                self._convencao = constantes.WINDOWS_STDCALL
            else:
                self._convencao = constantes.STANDARD_C

        if self._convencao == constantes.STANDARD_C:
            loader = ctypes.CDLL

        elif self._convencao == constantes.WINDOWS_STDCALL:
            loader = ctypes.WinDLL

        else:
            raise ValueError((
                    'Convencao de chamada desconhecida: {!r}'
                ).format(self._convencao))

        self._libsat = loader(self._caminho)

    @property
    def ref(self):
        """Uma referência para a biblioteca SAT carregada."""
        return self._libsat

    @property
    def caminho(self):
        """Caminho completo para a biblioteca SAT."""
        return self._caminho

    @property
    def convencao(self):
        """Convenção de chamada para a biblioteca SAT.
        Deverá ser um dos valores disponíveis na contante
        :attr:`~satcomum.constantes.CONVENCOES_CHAMADA`.
        """
        return self._convencao
satcfe/entidades.py
class CFeCancelamento(Entidade):
    """
    Representa um CF-e de cancelamento.

    :param Destinatario destinatario: Opcional. Uma instância de
        :class:`Destinatario` contendo apenas os dados exigidos para a operação
        de cancelamento (ie. ``CPF`` ou ``CNPJ`` do destinatário).

    :param str chCanc: Chave de acesso do CF-e a ser cancelado. Deve ser
        precedido do literal ``CFe`` seguido dos quarenta e quatro dígitos
        que compõem a chave de acesso.

    :param str CNPJ: CNPJ da software house, desenvolvedora do aplicativo
        comercial, contendo apenas os dígitos do número e incluindo zeros não
        significativos, se for o caso (14 dígitos).

    :param str signAC: Assinatura do aplicativo comercial (344 dígitos).

    :param int numeroCaixa: Número do caixa ao qual o SAT está conectado.
        Normalmente este será o número do caixa de onde parte a solicitação de
        cancelamento. Deverá ser um número inteiro entre ``0`` e ``999``.

    """

    def __init__(self, destinatario=None, **kwargs):
        self._destinatario = destinatario
        super(CFeCancelamento, self).__init__(schema={
                'chCanc': {
                        'type': 'string',
                        'required': True,
                        'regex': r'^CFe\d{44}$',
                    },
                'CNPJ': {
                        'type': 'string',
                        'check_with': 'cnpj',
                        'required': True,
                    },
                'signAC': {
                        'type': 'string',
                        'check_with': 'assinatura_ac',
                        'required': True,
                    },
                'numeroCaixa': {
                        'type': 'integer',
                        'required': True,
                        'min': 0,
                        'max': 999,
                    },
            }, **kwargs)

    @property
    def destinatario(self):
        """O :class:`Destinatario` ou ``None``."""
        return self._destinatario

    def _xml(self, *args, **kwargs):
        self.erros.clear()
        return super(CFeCancelamento, self)._xml(*args, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        cfecanc = ET.Element('CFeCanc')

        infCFe = ET.SubElement(cfecanc, 'infCFe')
        infCFe.attrib['chCanc'] = self.chCanc

        ide = ET.SubElement(infCFe, 'ide')
        ET.SubElement(ide, 'CNPJ').text = self.CNPJ
        ET.SubElement(ide, 'signAC').text = self.signAC
        ET.SubElement(ide, 'numeroCaixa').text = '{:03d}'.format(
                self.numeroCaixa)

        ET.SubElement(infCFe, 'emit')

        dest = self.destinatario or Destinatario()
        infCFe.append(dest._xml(cancelamento=True))

        ET.SubElement(infCFe, 'total')

        return cfecanc
satcfe/entidades.py
class PISSN(Entidade):
    """
    Grupo de PIS para contribuíntes do Simples Nacional, CST 49 (``PISSN``,
    grupo ``Q05``).

    :param str CST:
    """

    def __init__(self, **kwargs):
        super(PISSN, self).__init__(schema={
                'CST': {
                        'type': 'string',
                        'required': True,
                        'allowed': [v for v, s in constantes.Q07_CST_PISSN],
                    },
            }, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        pissn = ET.Element('PISSN')
        ET.SubElement(pissn, 'CST').text = self.CST
        return pissn
satcfe/entidades.py
class LocalEntrega(Entidade):
    """
    Identificação do Local de Entrega (``entrega``, grupo ``G01``).

    :param str xLgr:
    :param str nro:
    :param str xCpl: Opcional.
    :param str xBairro:
    :param str xMun:
    :param str UF:

    """

    def __init__(self, **kwargs):
        super(LocalEntrega, self).__init__(schema={
                'xLgr': {  # G02
                        'type': 'string',
                        'required': True,
                        'minlength': 2,
                        'maxlength': 60,
                    },
                'nro': {  # G03
                        'type': 'string',
                        'required': True,
                        'minlength': 1,
                        'maxlength': 60,
                    },
                'xCpl': {  # G04
                        'type': 'string',
                        'required': False,
                        'minlength': 1,
                        'maxlength': 60,
                    },
                'xBairro': {  # G05
                        'type': 'string',
                        'required': True,
                        'minlength': 1,
                        'maxlength': 60,
                    },
                'xMun': {  # G06
                        'type': 'string',
                        'required': True,
                        'minlength': 2,
                        'maxlength': 60,
                    },
                'UF': {  # G07
                        'type': 'string',
                        'check_with': 'uf',
                        'required': True,
                    },
            }, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        entrega = ET.Element('entrega')
        ET.SubElement(entrega, 'xLgr').text = self.xLgr
        ET.SubElement(entrega, 'nro').text = self.nro

        if hasattr(self, 'xCpl'):
            ET.SubElement(entrega, 'xCpl').text = self.xCpl

        ET.SubElement(entrega, 'xBairro').text = self.xBairro
        ET.SubElement(entrega, 'xMun').text = self.xMun
        ET.SubElement(entrega, 'UF').text = self.UF

        return entrega
satcfe/entidades.py
class Destinatario(Entidade):
    """
    Identificação do destinatário do CF-e (``dest``, grupo ``E01``).

    :param str CNPJ: Número do CNPJ do destinatário, contendo apenas os
        digitos e incluindo os zeros não significativos. **Não deve ser
        informado se o ``CPF`` for informado.**

    :param str CPF: Número do CPF do destinatário, contendo apenas os digitos e
        incluindo os zeros não significativos. **Não deve ser informado se o
        ``CNPJ`` for informado.**

    :param str xNome: Opcional. Nome ou razão social do destinatário.
        O nome do destinatário será ignorado no XML do CF-e de cancelamento.

    """

    def __init__(self, **kwargs):
        super(Destinatario, self).__init__(schema={
                'CNPJ': {  # E02
                        'type': 'string',
                        'check_with': 'cnpj',
                        'excludes': 'CPF',
                        'required': False,
                    },
                'CPF': {  # E03
                        'type': 'string',
                        'check_with': 'cpf',
                        'excludes': 'CNPJ',
                        'required': False,
                    },
                'xNome': {  # E04
                        'type': 'string',
                        'required': False,
                        'minlength': 2,
                        'maxlength': 60,
                    }
            }, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        is_cancelamento = kwargs.pop('cancelamento', False)

        dest = ET.Element('dest')

        if not is_cancelamento:
            if hasattr(self, 'CNPJ'):
                ET.SubElement(dest, 'CNPJ').text = self.CNPJ

            if hasattr(self, 'CPF'):
                ET.SubElement(dest, 'CPF').text = self.CPF

            if hasattr(self, 'xNome'):
                ET.SubElement(dest, 'xNome').text = self.xNome

        return dest
satcfe/entidades.py
class Imposto(Entidade):
    """
    Grupo de tributos incidentes no produto ou serviço (``imposto``,
    grupo ``M01``).

    :param icms: Opcional. Deve ser uma instância de uma das classes dos
        grupos de ICMS (:class:`ICMS00`, :class:`ICMS40`, :class:`ICMSSN102`
        ou :class:`ICMSSN900`) se o item for um produto tributado pelo ICMS
        ou ``None`` em caso contrário.

    :param pis: Deve ser uma instância de uma das classes dos grupos de
        PIS (:class:`PISAliq`, :class:`PISQtde`, :class:`PISNT`, :class:`PISSN`
        ou :class:`PISOutr`).

    :param pisst: Opcional. Instância de :class:`PISST` ou ``None``.

    :param cofins: Deve ser uma instância de uma das classes dos grupos
        de COFINS (:class:`COFINSAliq`, :class:`COFINSQtde`, :class:`COFINSNT`,
        :class:`COFINSSN` ou :class:`COFINSOutr`).

    :param cofinsst: Opcional. Instância de :class:`COFINSST` ou ``None``.

    :param issqn: Opcional. Uma instância de :class:`ISSQN` se o item for
        um serviço tributado pelo ISSQN ou ``None`` em caso contrário.

    :param Decimal vItem12741: Opcional. Valor aproximado dos tributos do
        produto ou serviço, conforme a Lei 12.741/12.

    """

    def __init__(
            self,
            icms=None,
            pis=None,
            pisst=None,
            cofins=None,
            cofinsst=None,
            issqn=None,
            **kwargs):
        self._icms = icms
        self._pis = pis
        self._pisst = pisst
        self._cofins = cofins
        self._cofinsst = cofinsst
        self._issqn = issqn
        super(Imposto, self).__init__(schema={
                'vItem12741': {  # M02
                        'type': 'decimal',
                        'required': False,
                    }
            }, **kwargs)

    @property
    def icms(self):
        """
        Um dos grupos de ICMS (:class:`ICMS00`, :class:`ICMS40`,
        :class:`ICMSSN102` ou :class:`ICMSSN900`) se o item for um produto
        tributado pelo ICMS ou ``None`` em caso contrário.
        """
        return self._icms

    @property
    def pis(self):
        """
        Um dos grupos de PIS (:class:`PISAliq`, :class:`PISQtde`,
        :class:`PISNT`, :class:`PISSN` ou :class:`PISOutr`).
        """
        return self._pis

    @property
    def pisst(self):
        """
        O grupo do PIS Substituição Tributária (:class:`PISST`) se for o
        caso, ou ``None``.
        """
        return self._pisst

    @property
    def cofins(self):
        """
        Um dos grupos de COFINS (:class:`COFINSAliq`, :class:`COFINSQtde`,
        :class:`COFINSNT`, :class:`COFINSSN` ou :class:`COFINSOutr`).
        """
        return self._cofins

    @property
    def cofinsst(self):
        """
        O grupo do COFINS Substituição Tributária (:class:`COFINSST`) se
        for o caso, ou ``None``.
        """
        return self._cofinsst

    @property
    def issqn(self):
        """
        O grupo de ISSQN (:class:`ISSQN`) se o item for um serviço
        tributado pelo ISSQN ou ``None`` em caso contrário.
        """
        return self._issqn

    def _construir_elemento_xml(self, *args, **kwargs):
        if self.pis is None:
            raise cerberus.DocumentError((
                    '{:s} (grupo M01) atributo "pis" não pode ser None.'
                ).format(self.__class__.__name__))

        if self.cofins is None:
            raise cerberus.DocumentError((
                    '{:s} (grupo M01) atributo "cofins" não pode ser None.'
                ).format(self.__class__.__name__))

        imposto = ET.Element('imposto')

        if hasattr(self, 'vItem12741'):
            ET.SubElement(imposto, 'vItem12741').text = str(self.vItem12741)

        if self.icms is not None:
            icms = ET.SubElement(imposto, 'ICMS')
            icms.append(self.icms._xml())

        pis = ET.SubElement(imposto, 'PIS')
        pis.append(self.pis._xml())

        if self.pisst is not None:
            imposto.append(self.pisst._xml())

        cofins = ET.SubElement(imposto, 'COFINS')
        cofins.append(self.cofins._xml())

        if self.cofinsst is not None:
            imposto.append(self.cofinsst._xml())

        if self.issqn is not None:
            imposto.append(self.issqn._xml())

        return imposto
satcfe/entidades.py
class Detalhamento(Entidade):
    """
    Detalhamento do produto ou serviço do CF-e (``det``, grupo ``H01``).

    :param ProdutoServico produto: Produto ou serviço, como uma instância
        de :class:`ProdutoServico` ao qual o detalhamento se refere.

    :param Imposto imposto: O grupo de tributos incidentes no produto ou
        serviço ao qual o detalhamento se refere, como uma instância
        de :class:`Imposto`.

    :param str infAdProd: Opcional. Informações adicionais do produto ou
        serviço (norma referenciada, informações complementares, etc).

    .. note::

        O atributo XML ``nItem`` (``H02``) não é determinado aqui, mas
        atribuído automaticamente, conforme a sua posição na lista de
        :attr:`~CFeVenda.detalhamentos`.

    """

    def __init__(self, produto=None, imposto=None, **kwargs):
        self._produto = produto
        self._imposto = imposto
        super(Detalhamento, self).__init__(schema={
               'infAdProd': {  # V01
                        'type': 'string',
                        'required': False,
                        'minlength': 1,
                        'maxlength': 500,
                    },
            }, **kwargs)

    @property
    def produto(self):
        return self._produto

    @property
    def imposto(self):
        return self._imposto

    def _construir_elemento_xml(self, *args, **kwargs):
        det = ET.Element('det')
        det.attrib['nItem'] = str(kwargs.pop('nItem'))

        det.append(self.produto._xml())
        det.append(self.imposto._xml())

        if hasattr(self, 'infAdProd'):
            ET.SubElement(det, 'infAdProd').text = self.infAdProd

        return det
satcfe/rede.py
class ConfiguracaoRede(Entidade):
    """Uma entidade que contém os parâmetros de configurações da interface de
    rede do equipamento SAT. Uma instância desta classe é usada como argumento
    para :meth:`~satcfe.base._FuncoesSAT.configurar_interface_de_rede`.

    :param str tipoInter: Tipo de interface de rede que o equipamento SAT
        deverá utilizar. As opções de tipos de rede estão disponíveis na
        constante :attr:`~satcomum.constantes.REDE_TIPOINTER_OPCOES`.

    :param str SSID: Opcional. Nome da rede sem fio contendo até 32 caracteres.

    :param str seg: Opcional. Tipo de segurança da rede sem fio. As opções
        estão na constante :attr:`~satcomum.constantes.REDE_SEG_OPCOES`.

    :param str codigo: Opcional. Senha de acesso à rede sem fio, contendo
        até 64 caracteres.

    :param str tipoLan: Tipo da rede LAN. As opções estão disponíveis na
        constante :attr:`~satcomum.constantes.REDE_TIPOLAN_OPCOES`.

    :param str lanIP: Opcional. Endereço IP do equipamento SAT.

    :param str lanMask: Opcional. Máscara de sub-rede.

    :param str lanGW: Opcional. Endereço IP do gateway padrão.

    :param str lanDNS1: Opcional. Endereço IP do DNS primário.

    :param str lanDNS2: Opcional. Endereço IP do DNS secundário.

    :param str usuario: Opcional. Nome do usuário para obtenção do endereço
        IP, se necessário, contendo até 64 caracteres.

    :param str senha: Opcional. Senha do usuário para obtenção do endereço IP,
        relacionado ao parâmetro ``usuario``, se necessário, contendo até 32
        caracteres.

    :param str proxy: Opcional. Indica a configuração de proxy da rede.
        As opções estão disponíveis na
        constante :attr:`~satcomum.constantes.REDE_PROXY_OPCOES`.

    :param str proxy_ip: Opcional. Endereço IP do servidor proxy.

    :param int proxy_porta: Opcional. Número da porta por onde o servidor de
        proxy responde.

    :param str proxy_user: Opcional. Nome do usuário para acesso ao proxy, se
        necessário, contendo até 64 caracteres.

    :param str proxy_senha: Opcional. Senha do usuário para acesso ao proxy,
        relacionado ao parâmetro ``proxy_user``, se necessário, contendo
        até 64 caracteres.

    """

    def __init__(self, **kwargs):
        super(ConfiguracaoRede, self).__init__(schema={
                'tipoInter': {
                        'type': 'string',
                        'required': True,
                        'allowed': [v for v, s in REDE_TIPOINTER_OPCOES],
                    },
                'SSID': {
                        'type': 'string',
                        'required': False,
                        'minlength': 1,
                        'maxlength': 32,
                    },
                'seg': {
                        'type': 'string',
                        'required': False,
                        'allowed': [v for v, s in REDE_SEG_OPCOES],
                    },
                'codigo': {
                        'type': 'string',
                        'required': False,
                        'minlength': 1,
                        'maxlength': 64,
                    },
                'tipoLan': {
                        'type': 'string',
                        'required': True,
                        'allowed': [v for v, s in REDE_TIPOLAN_OPCOES],
                    },
                'lanIP': {
                        'type': 'string',
                        'check_with': 'ipv4',
                        'required': False,
                    },
                'lanMask': {
                        'type': 'string',
                        'check_with': 'ipv4',
                        'required': False,
                    },
                'lanGW': {
                        'type': 'string',
                        'check_with': 'ipv4',
                        'required': False,
                    },
                'lanDNS1': {
                        'type': 'string',
                        'check_with': 'ipv4',
                        'required': False,
                    },
                'lanDNS2': {
                        'type': 'string',
                        'check_with': 'ipv4',
                        'required': False,
                    },
                'usuario': {
                        'type': 'string',
                        'required': False,
                        'minlength': 1, 'maxlength': 64,
                    },
                'senha': {
                        'type': 'string',
                        'required': False,
                        'minlength': 1,
                        'maxlength': 64,
                    },
                'proxy': {
                        'type': 'string',
                        'required': False,
                        'allowed': [v for v, s in REDE_PROXY_OPCOES],
                    },
                'proxy_ip': {
                        'type': 'string',
                        'check_with': 'ipv4',
                        'required': False,
                    },
                'proxy_porta': {
                        'type': 'integer',
                        'required': False,
                        'min': 0,
                        'max': 65535,
                    },
                'proxy_user': {
                        'type': 'string',
                        'required': False,
                        'minlength': 1,
                        'maxlength': 64,
                    },
                'proxy_senha': {
                        'type': 'string',
                        'required': False,
                        'minlength': 1,
                        'maxlength': 64,
                    },
            }, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        config = ET.Element('config')
        for elemento in self._schema.keys():
            valor = getattr(self, elemento, None)
            if valor:
                if isinstance(valor, int):
                    valor = str(valor)
                ET.SubElement(config, elemento).text = valor
        return config
satcfe/entidades.py
class MeioPagamento(Entidade):
    """Meio de pagamento (``MP``, grupo ``WA02``).

    :param str cMP:
    :param Decimal vMP:
    :param str cAdmC: Opcional.
    """

    def __init__(self, **kwargs):
        super(MeioPagamento, self).__init__(schema={
                'cMP': {
                        'type': 'string',
                        'required': True,
                        'allowed': [v for v, s in constantes.WA03_CMP_MP],
                    },
                'vMP': {
                        'type': 'decimal',
                        'required': True,
                    },
                'cAdmC': {
                        'type': 'string',
                        'required': False,
                        'allowed': [
                                codigo
                                for codigo, cnpj, nome
                                in constantes.CREDENCIADORAS_CARTAO
                            ],
                    },
            }, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        mp = ET.Element('MP')
        ET.SubElement(mp, 'cMP').text = self.cMP
        ET.SubElement(mp, 'vMP').text = str(self.vMP)
        if hasattr(self, 'cAdmC'):
            ET.SubElement(mp, 'cAdmC').text = self.cAdmC
        return mp
satcfe/clientelocal.py
class ClienteSATLocal(FuncoesSAT):
    """Fornece acesso ao equipamento SAT conectado na máquina local.

    As respostas às funções SAT serão trabalhadas resultando em objetos Python
    regulares cujos atributos representam as peças de informação conforme
    descrito, função por função, na ER SAT.
    """

    def __init__(self, *args, **kwargs):
        super(ClienteSATLocal, self).__init__(*args, **kwargs)

    def ativar_sat(self, tipo_certificado, cnpj, codigo_uf):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.ativar_sat`.

        :return: Uma resposta SAT especilizada em ``AtivarSAT``.
        :rtype: satcfe.resposta.ativarsat.RespostaAtivarSAT
        """
        retorno = super(ClienteSATLocal, self).ativar_sat(
                tipo_certificado,
                cnpj,
                codigo_uf)
        return RespostaAtivarSAT.analisar(retorno)

    def comunicar_certificado_icpbrasil(self, certificado):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.comunicar_certificado_icpbrasil`.

        :return: Uma resposta SAT padrão.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).comunicar_certificado_icpbrasil(
                certificado)
        return RespostaSAT.comunicar_certificado_icpbrasil(retorno)

    def enviar_dados_venda(self, dados_venda, *args, **kwargs):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.enviar_dados_venda`.

        :return: Uma resposta SAT especializada em ``EnviarDadosVenda``.
        :rtype: satcfe.resposta.enviardadosvenda.RespostaEnviarDadosVenda
        """
        retorno = super(ClienteSATLocal, self).enviar_dados_venda(
                dados_venda,
                *args,
                **kwargs)
        return RespostaEnviarDadosVenda.analisar(retorno)

    def cancelar_ultima_venda(
            self,
            chave_cfe,
            dados_cancelamento,
            *args,
            **kwargs):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.cancelar_ultima_venda`.

        :return: Uma resposta SAT especializada em ``CancelarUltimaVenda``.
        :rtype: satcfe.resposta.cancelarultimavenda.RespostaCancelarUltimaVenda
        """
        retorno = super(ClienteSATLocal, self).cancelar_ultima_venda(
                chave_cfe,
                dados_cancelamento,
                *args,
                **kwargs)
        return RespostaCancelarUltimaVenda.analisar(retorno)

    def consultar_sat(self):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.consultar_sat`.

        :return: Uma resposta SAT padrão.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).consultar_sat()
        return RespostaSAT.consultar_sat(retorno)

    def teste_fim_a_fim(self, dados_venda, *args, **kwargs):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.teste_fim_a_fim`.

        :return: Uma resposta SAT especializada em ``TesteFimAFim``.
        :rtype: satcfe.resposta.testefimafim.RespostaTesteFimAFim
        """
        retorno = super(ClienteSATLocal, self).teste_fim_a_fim(
                dados_venda,
                *args,
                **kwargs)
        return RespostaTesteFimAFim.analisar(retorno)

    def consultar_status_operacional(self):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.consultar_status_operacional`.

        :return: Uma resposta SAT especializada em
            ``ConsultarStatusOperacional``.
        :rtype: satcfe.resposta.consultarstatusoperacional.RespostaConsultarStatusOperacional
        """  # noqa: E501
        retorno = super(ClienteSATLocal, self).consultar_status_operacional()
        return RespostaConsultarStatusOperacional.analisar(retorno)

    def consultar_numero_sessao(self, numero_sessao):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.consultar_numero_sessao`.

        :return: Uma resposta SAT que irá depender da sessão consultada.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).consultar_numero_sessao(
                numero_sessao)
        return RespostaConsultarNumeroSessao.analisar(retorno)

    def configurar_interface_de_rede(self, configuracao, *args, **kwargs):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.configurar_interface_de_rede`.

        :return: Uma resposta SAT padrão.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).configurar_interface_de_rede(
                configuracao,
                *args,
                **kwargs)
        return RespostaSAT.configurar_interface_de_rede(retorno)

    def associar_assinatura(self, sequencia_cnpj, assinatura_ac):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.associar_assinatura`.

        :return: Uma resposta SAT padrão.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).associar_assinatura(
                sequencia_cnpj,
                assinatura_ac)
        return RespostaAssociarAssinatura.analisar(retorno)

    def atualizar_software_sat(self):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.atualizar_software_sat`.

        :return: Uma resposta SAT padrão.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).atualizar_software_sat()
        return RespostaSAT.atualizar_software_sat(retorno)

    def extrair_logs(self):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.extrair_logs`.

        :return: Uma resposta SAT especializada em ``ExtrairLogs``.
        :rtype: satcfe.resposta.extrairlogs.RespostaExtrairLogs
        """
        retorno = super(ClienteSATLocal, self).extrair_logs()
        return RespostaExtrairLogs.analisar(retorno)

    def bloquear_sat(self):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.bloquear_sat`.

        :return: Uma resposta SAT padrão.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).bloquear_sat()
        return RespostaSAT.bloquear_sat(retorno)

    def desbloquear_sat(self):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.desbloquear_sat`.

        :return: Uma resposta SAT padrão.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).desbloquear_sat()
        return RespostaSAT.desbloquear_sat(retorno)

    def trocar_codigo_de_ativacao(
            self, novo_codigo_ativacao,
            opcao=constantes.CODIGO_ATIVACAO_REGULAR,
            codigo_emergencia=None):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.trocar_codigo_de_ativacao`.

        :return: Uma resposta SAT padrão.
        :rtype: satcfe.resposta.padrao.RespostaSAT
        """
        retorno = super(ClienteSATLocal, self).trocar_codigo_de_ativacao(
                novo_codigo_ativacao,
                opcao=opcao,
                codigo_emergencia=codigo_emergencia)

        resposta = RespostaSAT.trocar_codigo_de_ativacao(retorno)

        # NOTA: Se não foi lançada nenhuma exceção, assume que a troca do
        # código de ativação foi bem sucedida. Alterna o código de ativação
        # atual para o novo código de ativação para que as chamadas
        # subsequentes tenham sucesso.
        self._codigo_ativacao = novo_codigo_ativacao

        return resposta

    def consultar_ultima_sessao_fiscal(self):
        """Sobrepõe :meth:`~satcfe.base.FuncoesSAT.consultar_ultima_sessao_fiscal`.

        :return: Uma resposta SAT que irá depender do último comando "fiscal"
            executado pelo equipmamento SAT, que poderá ser uma venda ou um
            cancelamento de venda.

        :rtype: satcfe.resposta.consultarultimasessaofiscal.RespostaConsultarUltimaSessaoFiscal |
            satcfe.resposta.enviardadosvenda.RespostaEnviarDadosVenda |
            satcfe.resposta.cancelarultimavenda.RespostaCancelarUltimaVenda

        """  # noqa: E501
        retorno = super(ClienteSATLocal, self).consultar_ultima_sessao_fiscal()
        return RespostaConsultarUltimaSessaoFiscal.analisar(retorno)
satcfe/entidades.py
class ProdutoServico(Entidade):
    """
    Produto ou serviço do CF-e (``prod``, grupo ``I01``).

    :param str cProd:
    :param str cEAN: Opcional.
    :param str xProd:
    :param str NCM: Opcional.
    :param str CFOP:
    :param str uCom:
    :param Decimal qCom:
    :param Decimal vUnCom:
    :param str indRegra:
    :param Decimal vDesc: Opcional.
    :param Decimal vOutro: Opcional.
    :param list observacoes_fisco: Opcional. Lista de objetos
        :class:`ObsFiscoDet` representando os campos de uso livre do fisco.

    """

    def __init__(self, observacoes_fisco=None, **kwargs):
        self._observacoes_fisco = observacoes_fisco
        super(ProdutoServico, self).__init__(schema={
                'cProd': {  # I02
                        'type': 'string',
                        'required': True,
                        'minlength': 1,
                        'maxlength': 60,
                    },
                'cEAN': {  # I03
                        'type': 'string',
                        'required': False,
                        'regex': r'^(\d{8}|\d{12}|\d{13}|\d{14})$',
                    },
                'xProd': {  # I04
                        'type': 'string',
                        'required': True,
                        'minlength': 1,
                        'maxlength': 120,
                    },
                'NCM': {  # I05
                        'type': 'string',
                        'required': False,
                        'regex': r'^(\d{2}|\d{8})$',
                    },
                'CFOP': {  # I06
                        'type': 'string',
                        'required': True,
                        'regex': r'^\d{4}$',
                    },
                'uCom': {  # I07
                        'type': 'string',
                        'required': True,
                        'minlength': 1,
                        'maxlength': 6,
                    },
                'qCom': {  # I08
                        'type': 'decimal',
                        'required': True,
                    },
                'vUnCom': {  # I09
                        'type': 'decimal',
                        'required': True,
                    },
                'indRegra': {  # I11
                        'type': 'string',
                        'required': True,
                        'allowed': [v for v, s in constantes.I11_INDREGRA],
                    },
                'vDesc': {  # I12
                        'type': 'decimal',
                        'required': False,
                    },
                'vOutro': {  # I13
                        'type': 'decimal',
                        'required': False,
                    },
            }, **kwargs)

    @property
    def observacoes_fisco(self):
        return tuple(self._observacoes_fisco or ())

    def _construir_elemento_xml(self, *args, **kwargs):
        prod = ET.Element('prod')
        ET.SubElement(prod, 'cProd').text = self.cProd

        if hasattr(self, 'cEAN'):
            ET.SubElement(prod, 'cEAN').text = self.cEAN

        ET.SubElement(prod, 'xProd').text = self.xProd

        if hasattr(self, 'NCM'):
            ET.SubElement(prod, 'NCM').text = self.NCM

        ET.SubElement(prod, 'CFOP').text = self.CFOP
        ET.SubElement(prod, 'uCom').text = self.uCom
        ET.SubElement(prod, 'qCom').text = str(self.qCom)
        ET.SubElement(prod, 'vUnCom').text = str(self.vUnCom)
        ET.SubElement(prod, 'indRegra').text = self.indRegra

        if hasattr(self, 'vDesc'):
            ET.SubElement(prod, 'vDesc').text = str(self.vDesc)

        if hasattr(self, 'vOutro'):
            ET.SubElement(prod, 'vOutro').text = str(self.vOutro)

        if self.observacoes_fisco:
            for obs in self.observacoes_fisco:
                prod.append(obs._xml())

        return prod
satcfe/entidades.py
class Emitente(Entidade):
    """
    Identificação do emitente do CF-e (``emit``, grupo ``C01``).

    :param str CNPJ: Número do CNPJ do emitente do CF-e, contendo apenas os
        digitos e incluindo os zeros não significativos.

    :param str IE: Número de Inscrição Estadual do emitente do CF-e, contendo
        apenas digitos.

    :param str IM: Opcional. Deve ser informado o número da Inscrição
        Municipal quando o CF-e possuir itens com prestação de serviços
        sujeitos ao ISSQN, por exemplo.

    :param str cRegTribISSQN: Opcional. Indica o regime especial de
        tributação do ISSQN. Veja as constantes em
        :attr:`~satcomum.constantes.C15_CREGTRIBISSQN_EMIT`.

    :param str indRatISSQN: Opcional. Indicador de rateio do desconto sobre o
        subtotal entre itens sujeitos à tributação pelo ISSQN. Veja as
        constantes em :attr:`~satcomum.constantes.C16_INDRATISSQN_EMIT`.

    """

    def __init__(self, **kwargs):
        super(Emitente, self).__init__(schema={
                'CNPJ': {
                        'type': 'string',
                        'check_with': 'cnpj',
                        'required': True,
                    },
                'IE': {
                        'type': 'string',
                        'required': True,
                        'regex': r'^\d{2,12}$'
                    },
                'IM': {
                        'type': 'string',
                        'required': False,
                        'regex': r'^\d{1,15}$'
                    },
                'cRegTribISSQN': {
                        'type': 'string',
                        'required': False,
                        'allowed': [
                                v for v, s in constantes.C15_CREGTRIBISSQN_EMIT
                            ],
                    },
                'indRatISSQN': {
                        'type': 'string',
                        'required': True,
                        'allowed': [
                                v for v, s in constantes.C16_INDRATISSQN_EMIT
                            ],
                    },
            }, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        emit = ET.Element('emit')
        ET.SubElement(emit, 'CNPJ').text = self.CNPJ
        ET.SubElement(emit, 'IE').text = self.IE

        if hasattr(self, 'IM'):
            ET.SubElement(emit, 'IM').text = self.IM

        if hasattr(self, 'cRegTribISSQN'):
            ET.SubElement(emit, 'cRegTribISSQN').text = self.cRegTribISSQN

        ET.SubElement(emit, 'indRatISSQN').text = self.indRatISSQN

        return emit
satcfe/entidades.py
class CFeVenda(Entidade):
    """
    Representa um CF-e de venda.

    :param Emitente emitente: Identificação do emitente do CF-e.

    :param Destinatario destinatario: Opcional. Identificação do
        destinatário.

    :param LocalEntrega entrega: Opcional. Informações do local de entrega.

    :param list detalhamentos: Uma lista de objetos :class:`ProdutoServico` que
        representam os produtos/serviços participantes do CF-e de venda.

    :param DescAcrEntr descontos_acrescimos_subtotal: Opcional. Se informado,
        deverá ser um objeto :class:`DescAcrEntr` que contenha o valor de
        desconto ou acréscimo sobre o subtotal.

    :param list pagamentos: Uma lista de objetos :class:`MeioPagamento` que
        descrevem cada um dos meios de pagamentos usados no CF-e de venda.

    :param InformacoesAdicionais informacoes_adicionais: Opcional.

    :param str versaoDadosEnt: Opcional. String contendo a versão do layout
        do arquivo de dados do aplicativo comercial. Se não informado será
        utilizado o valor da constante ``VERSAO_LAYOUT_ARQUIVO_DADOS_AC`` do
        módulo ``constantes`` do
        `projeto ``satcomum`` <https://github.com/base4sistemas/satcomum/>`_

    :param str CNPJ:  CNPJ da software house, desenvolvedora do aplicativo
        comercial, contendo apenas os dígitos do número e incluindo zeros não
        significativos, se for o caso (14 dígitos).

    :param str signAC:  Assinatura do aplicativo comercial (344 dígitos).

    :param int numeroCaixa: Número do caixa ao qual o SAT está conectado.
        Normalmente este será o número do caixa de onde parte a solicitação de
        cancelamento. Deverá ser um número inteiro entre ``0`` e ``999``.

    :param Decimal vCFeLei12741: Opcional. Se informado deve representar a
        soma total dos valores aproximados dos tributos, em cumprimento à Lei
        nº 12.741/2012.

    ..note::

        Não há uma classe específica para representar o elemento ``ide``
        do grupo ``B01``, já que todos os seus atributos são esperados nesta
        classe.

    """

    def __init__(
            self,
            emitente=None,
            destinatario=None,
            entrega=None,
            detalhamentos=None,
            descontos_acrescimos_subtotal=None,
            pagamentos=None,
            informacoes_adicionais=None,
            **kwargs):

        self._emitente = emitente
        self._destinatario = destinatario
        self._entrega = entrega
        self._detalhamentos = detalhamentos
        self._descontos_acrescimos_subtotal = descontos_acrescimos_subtotal
        self._pagamentos = pagamentos
        self._informacoes_adicionais = informacoes_adicionais

        super(CFeVenda, self).__init__(
                versaoDadosEnt=constantes.VERSAO_LAYOUT_ARQUIVO_DADOS_AC,
                schema={
                        'versaoDadosEnt': {
                                'type': 'string',
                                'required': True,
                                'regex': r'^\d{1}\.\d{2}$',
                            },
                        'CNPJ': {
                                'type': 'string',
                                'check_with': 'cnpj',
                                'required': True,
                            },
                        'signAC': {
                                'type': 'string',
                                'check_with': 'assinatura_ac',
                                'required': True,
                            },
                        'numeroCaixa': {
                                'type': 'integer',
                                'required': True,
                                'min': 0,
                                'max': 999,
                            },
                        'vCFeLei12741': {
                                'type': 'decimal',
                                'required': False,
                            },
                    }, **kwargs)

    @property
    def emitente(self):
        """O :class:`Emitente` do CF-e."""
        return self._emitente

    @property
    def destinatario(self):
        """O :class:`Destinatario` do CF-e ou ``None``."""
        return self._destinatario

    @property
    def entrega(self):
        """O Local de entrega (:class:`LocalEntrega`) ou ``None``."""
        return self._entrega

    @property
    def detalhamentos(self):
        """
        Lista de objetos :class:`Detalhamento`, descrevendo os produtos e
        serviços do CF-e.
        """
        return tuple(self._detalhamentos or ())

    @property
    def descontos_acrescimos_subtotal(self):
        """
        Os descontos e acréscimos no subtotal do CF-e (:class:`DescAcrEntr`)
        ou ``None``.
        """
        return self._descontos_acrescimos_subtotal

    @property
    def pagamentos(self):
        """
        Lista de objetos :class`MeioPagamento`, descrevendo os meios de
        pagamento empregados na quitação do CF-e.
        """
        return tuple(self._pagamentos or ())

    @property
    def informacoes_adicionais(self):
        """
        Informações adicionais do CF-e (:class:`InformacoesAdicionais`)
        ou ``None``.
        """
        return self._informacoes_adicionais

    def _xml(self, *args, **kwargs):
        self.erros.clear()
        return super(CFeVenda, self)._xml(*args, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        cfe = ET.Element('CFe')
        infCFe = ET.SubElement(cfe, 'infCFe')
        infCFe.attrib['versaoDadosEnt'] = self.versaoDadosEnt

        ide = ET.SubElement(infCFe, 'ide')
        ET.SubElement(ide, 'CNPJ').text = self.CNPJ
        ET.SubElement(ide, 'signAC').text = self.signAC
        ET.SubElement(ide, 'numeroCaixa').text = '{:03d}'.format(
                self.numeroCaixa)

        infCFe.append(self.emitente._xml())

        dest = self.destinatario or Destinatario()
        infCFe.append(dest._xml())

        if self.entrega is not None:
            infCFe.append(self.entrega._xml())

        if self.detalhamentos:
            for n, det in enumerate(self.detalhamentos):
                infCFe.append(det._xml(nItem=n+1))

        total = ET.SubElement(infCFe, 'total')

        if hasattr(self, 'vCFeLei12741'):
            ET.SubElement(total, 'vCFeLei12741').text = str(self.vCFeLei12741)

        if self.descontos_acrescimos_subtotal is not None:
            total.append(self.descontos_acrescimos_subtotal._xml())

        pgto = ET.SubElement(infCFe, 'pgto')
        if self.pagamentos:
            for pg in self.pagamentos:
                pgto.append(pg._xml())

        if self.informacoes_adicionais is not None:
            infCFe.append(self.informacoes_adicionais._xml())

        return cfe
satcfe/entidades.py
class COFINSSN(Entidade):
    """
    Grupo de COFINS para contribuíntes do Simples Nacional, CST 49
    (``COFINSSN``, grupo ``S05``).

    :param str CST:
    """

    def __init__(self, **kwargs):
        super(COFINSSN, self).__init__(schema={
                'CST': {
                        'type': 'string',
                        'required': True,
                        'allowed': [v for v, s in constantes.S07_CST_COFINSSN],
                    },
            }, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        cofinssn = ET.Element(self.__class__.__name__)
        ET.SubElement(cofinssn, 'CST').text = self.CST
        return cofinssn
satcfe/entidades.py
class ICMSSN102(Entidade):
    """
    Grupo de tributação do ICMS Simples Nacional, CSOSN 102, 300, 400 e 500
    (``ICMSSN102``, grupo ``N04``).

    :param str Orig:
    :param str CSOSN:
    """

    def __init__(self, **kwargs):
        super(ICMSSN102, self).__init__(schema={
                'Orig': {  # N06
                        'type': 'string',
                        'required': True,
                        'allowed': [v for v, s in constantes.N06_ORIG],
                    },
                'CSOSN': {  # N10
                        'type': 'string',
                        'required': True,
                        'allowed': [
                                v for v, s in constantes.N10_CSOSN_ICMSSN102
                            ],
                    },
            }, **kwargs)

    def _construir_elemento_xml(self, *args, **kwargs):
        icmssn102 = ET.Element('ICMSSN102')
        ET.SubElement(icmssn102, 'Orig').text = self.Orig
        ET.SubElement(icmssn102, 'CSOSN').text = self.CSOSN
        return icmssn102


Question: import os
import re
import shutil
import pytest
from collections import namedtuple
from decimal import Decimal
from builtins import str as text
from unidecode import unidecode
from satcomum import constantes
from satcfe.base import BibliotecaSAT
from satcfe.clientelocal import ClienteSATLocal
from satcfe.entidades import CFeCancelamento
from satcfe.entidades import CFeVenda
from satcfe.entidades import Destinatario
from satcfe.entidades import Emitente
from satcfe.entidades import LocalEntrega
from satcfe.entidades import Detalhamento
from satcfe.entidades import ProdutoServico
from satcfe.entidades import Imposto
from satcfe.entidades import ICMSSN102
from satcfe.entidades import PISSN
from satcfe.entidades import COFINSSN
from satcfe.entidades import MeioPagamento
from satcfe.rede import ConfiguracaoRede
    for el in _MARKERS:
        option_name = _to_cmd_line_option(el.name)
        if not config.getoption(option_name):
            markers_set[el.name] = pytest.mark.skip(reason=el.reason)

    for item in items:
        for key, marker in markers_set.items():
            if key in item.keywords:
                item.add_marker(marker)


@pytest.fixture(scope='function')
def datadir(tmpdir, request):
    """Este fixture procura por um diretório de dados com o mesmo nome do
    módulo de teste (mas sem a extensão ".py" e com o prefixo "test_" e/ou o
    sufixo "_test" removidos) dentro de ``satcfe/tests/data/``.

    Por exemplo, o módulo de testes ``tests/test_ativarsat.py`` possui um
    diretório de dados em ``tests/data/ativarsat/``.

    Existindo um diretório de dados, todo seu conteúdo será copiado para o
    diretório temporário (*fixture* ``tmpdir`` de pytest), de modo que os
    testes possam usar esses arquivos.

    No exemplo abaixo, os arquivos ``data.txt`` e ``huge.txt`` deverão ser
    criados no diretório ``tests/data/foo/``. Note que o diretório ``data/``
    está no mesmo diretório em que módulo ``test_foo.py`` se encontra:

    .. sourcecode:: python

        def test_foo(datadir):
            with open(datadir.join('data.txt'), 'r') as f_data, \
                 open(datadir.join('huge.txt'), 'r') as f_huge:
                data = f_data.read()
                expected_result = f_huge.read()
            result = do_something_with(data)
            assert result == expected_result

    .. note::

        Baseado `nesta resposta <https://stackoverflow.com/a/29631801>`_ de
        Stack Overflow.

    """
    path, name = os.path.split(request.module.__file__)
    name, _ = os.path.splitext(name)
    name = re.sub(r'^test_|_test$', '', name)
    dirname = os.path.join(path, 'data', name)

    if os.path.isdir(dirname):
        for name in os.listdir(dirname):
            filename = os.path.join(dirname, name)
            if os.path.isfile(filename):
                # copia apenas arquivos regulares (os.path.isfile)
                dst = text(tmpdir)
                shutil.copy(filename, dst)

    return tmpdir


@pytest.fixture(scope='module')
def clientesatlocal(request):
    funcoes = ClienteSATLocal(
            BibliotecaSAT(
                    request.config.getoption('--lib-caminho'),
                    convencao=request.config.getoption('--lib-convencao')),
            codigo_ativacao=request.config.getoption('--codigo-ativacao'))
    return funcoes


@pytest.fixture(scope='module')
def cfevenda(request):
    _opcao = request.config.getoption
    cfe = CFeVenda(
            CNPJ=_opcao('--cnpj-ac'),
            signAC=_opcao('--assinatura-ac'),
            numeroCaixa=_opcao('--numero-caixa'),
            emitente=Emitente(
                    CNPJ=_opcao('--emitente-cnpj'),
                    IE=_opcao('--emitente-ie'),
                    IM=_opcao('--emitente-im'),
                    cRegTribISSQN=_opcao('--emitente-issqn-regime'),
                    indRatISSQN=_opcao('--emitente-issqn-rateio')),
            destinatario=Destinatario(
                    CPF='11122233396',
                    xNome=u'João de Teste'),
            entrega=LocalEntrega(
                    xLgr='Rua Armando Gulim',
                    nro='65',
                    xBairro=u'Parque Glória III',
                    xMun='Catanduva',
                    UF='SP'),
            detalhamentos=[
                    Detalhamento(
                            produto=ProdutoServico(
                                    cProd='123456',
                                    xProd='BORRACHA STAEDTLER pvc-free',
                                    CFOP='5102',
                                    uCom='UN',
                                    qCom=Decimal('1.0000'),
                                    vUnCom=Decimal('5.75'),
                                    indRegra='A'),
                            imposto=Imposto(
                                    icms=ICMSSN102(Orig='2', CSOSN='500'),
                                    pis=PISSN(CST='49'),
                                    cofins=COFINSSN(CST='49'))),
                ],
            pagamentos=[
                    MeioPagamento(
                            cMP=constantes.WA03_DINHEIRO,
                            vMP=Decimal('10.00')),
                ])
    return cfe


@pytest.fixture(scope='module')
def cfecancelamento(request):
    _opcao = request.config.getoption
    # (!) Talvez seja necessário atribuir valor ao atributo 'chCanc' nas
    #     funcoes que utilizarem esta fixture.

Answer:     cfe_canc = CFeCancelamento(