diff --git a/pynfe/entidades/__init__.py b/pynfe/entidades/__init__.py index 7ebdc55..43fa430 100644 --- a/pynfe/entidades/__init__.py +++ b/pynfe/entidades/__init__.py @@ -4,4 +4,5 @@ from cliente import Cliente from transportadora import Transportadora from notafiscal import NotaFiscal from lotes import LoteNotaFiscal +from fontes_dados import FonteDados diff --git a/pynfe/entidades/base.py b/pynfe/entidades/base.py index 1988de4..bec7cc1 100644 --- a/pynfe/entidades/base.py +++ b/pynfe/entidades/base.py @@ -1,12 +1,18 @@ # -*- coding: utf-8 -*- class Entidade(object): + _fonte_dados = None + def __init__(self, **kwargs): # Codigo para dinamizar a criacao de instancias de entidade, # aplicando os valores dos atributos na instanciacao - for k, v in kwargs: + for k, v in kwargs.items(): setattr(self, k, v) + # Adiciona o objeto à fonte de dados informada + if self._fonte_dados: + self._fonte_dados.adicionar_objeto(self) + class Lote(object): pass diff --git a/pynfe/entidades/emitente.py b/pynfe/entidades/emitente.py index 49b0678..b4ba179 100644 --- a/pynfe/entidades/emitente.py +++ b/pynfe/entidades/emitente.py @@ -54,3 +54,6 @@ class Emitente(Entidade): # Logotipo logotipo = None + def __repr__(self): + return ''%(self.cnpj, self.razao_social) + diff --git a/pynfe/entidades/fontes_dados.py b/pynfe/entidades/fontes_dados.py new file mode 100644 index 0000000..bb6b159 --- /dev/null +++ b/pynfe/entidades/fontes_dados.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +from pynfe.excecoes import NenhumObjetoEncontrado, MuitosObjetosEncontrados + +class FonteDados(object): + u"""Classe responsável por ser o repositório dos objetos em memória e que + pode ser extendida para persistir esses objetos. Também tem a função de + memorizar os objetos redundantes como um só e assim otimizar o desempenho.""" + + _objetos = None + + def __init__(self, objetos=None): + # Inicializa variável que armazena os objetos contidos na Fonte de Dados + if objetos: + self._objetos = objetos + else: + self._objetos = [] + + def carregar_objetos(self, **kwargs): + u"""Método responsavel por retornar os objetos que casem com os atributos + informados no argumento **kwargs (argumentos nomeados). + + Um argumento especial é o '_classe', que representa a classe da entidade + desejada. + + FIXME: Este algoritimo pode ser melhorado pra fazer pesquisas melhores, + mas por enquanto vamos nos focar no processo em geral para só depois nos + preocupar com otimizações e desempenho.""" + + # Função de filtro + def filtrar(obj): + ret = True + + for k,v in kwargs.items(): + # Filtra pela classe e pelos atributos + ret = (k == '_classe' and isinstance(obj, v)) or\ + (k != '_classe' and getattr(obj, k, None) == v) + + if not ret: + break + + return ret + + # Filtra a lista de objetos + lista = filter(filtrar, self._objetos) + + return lista + + def adicionar_objeto(self, _objeto): + u"""Método responsável por adicionar o(s) objeto(s) informado(s) ao + repositorio de objetos da fonte de dados.""" + + from base import Entidade + + # Adiciona _objeto como objeto + if isinstance(_objeto, Entidade): + self._objetos.append(_objeto) + + # Adiciona _objeto como lista + elif isinstance(_objeto, (list, tuple)): + self._objetos += _objeto + + else: + raise Exception('Objeto informado e invalido!') + + def remover_objeto(self, _objeto=None, **kwargs): + u"""Método responsavel por remover os objetos que casem com os atributos + informados no argumento **kwargs (argumentos nomeados). + + Um argumento especial é o '_classe', que representa a classe da entidade + desejada. + + Outro argumetno especial é o '_objeto', que representa o objeto a ser + removido. Caso o argumento _objeto seja uma lista de objetos, eles serão + removidos também.""" + + from base import Entidade + + lista = None + + # Remove objetos + if not _objeto: + lista = self.carregar_objetos(**kwargs) + + # Remove _objeto como objeto + elif isinstance(_objeto, Entidade): + lista = [_objeto] + + # Remove _objeto como objeto + elif isinstance(_objeto, (list, tuple)): + lista = _objeto + + else: + raise Exception('Objeto informado e invalido!') + + # Efetiva a remoção + for obj in lista: + self._objetos.remove(obj) + + def obter_objeto(self, **kwargs): + u"""Faz a ponte para o método 'carregar_objetos' mas obriga o retorno de + apenas um objeto, levantando exceção se nenhum for encontrado ou se forem + encontrados mais de um.""" + + lista = self.carregar_objetos(**kwargs) + + if len(lista) == 0: + raise NenhumObjetoEncontrado('Nenhum objeto foi encontrado!') + elif len(lista) > 1: + raise MuitosObjetosEncontrados('Muitos objetos foram encontrados!') + + return lista[0] + + def obter_lista(self, **kwargs): + u"""Método de proxy, que somente repassa a chamada ao metodo 'carregar_objetos'""" + return self.carregar_objetos(**kwargs) + + def contar_objetos(self, **kwargs): + u"""Método que repassa a chamada ao metodo 'carregar_objetos' mas retorna + somente a quantidade de objetos encontrados.""" + + if kwargs: + return len(self.carregar_objetos(**kwargs)) + else: + return len(self._objetos) + + diff --git a/pynfe/excecoes.py b/pynfe/excecoes.py new file mode 100644 index 0000000..cd9250e --- /dev/null +++ b/pynfe/excecoes.py @@ -0,0 +1,6 @@ +class NenhumObjetoEncontrado(Exception): + pass + +class MuitosObjetosEncontrados(Exception): + pass + diff --git a/pynfe/processamento/interfaces.py b/pynfe/processamento/serializacao.py similarity index 52% rename from pynfe/processamento/interfaces.py rename to pynfe/processamento/serializacao.py index eeff24a..c5ddab0 100644 --- a/pynfe/processamento/interfaces.py +++ b/pynfe/processamento/serializacao.py @@ -19,7 +19,7 @@ except ImportError: except ImportError: raise Exception('Falhou ao importar lxml/ElementTree') -class Interface(object): +class Serializacao(object): """Classe abstrata responsavel por fornecer as funcionalidades basicas para exportacao e importacao de Notas Fiscais eletronicas para formatos serializados de arquivos. Como XML, JSON, binario, etc. @@ -29,7 +29,7 @@ class Interface(object): lista_de_nfs = None def __new__(cls, *args, **kwargs): - if cls == Interface: + if cls == Serializacao: raise Exception('Esta classe nao pode ser instanciada diretamente!') else: return cls(*args, **kwargs) @@ -49,6 +49,52 @@ class Interface(object): raise Exception('Metodo nao implementado') -class InterfaceXML(Interface): - pass +class SerializacaoXML(Serializacao): + def exportar(self, objetos, destino): + """Gera o(s) arquivo(s) de Nofa Fiscal eletronica no padrao oficial da SEFAZ + e Receita Federal, para ser(em) enviado(s) para o webservice ou para ser(em) + armazenado(s) em cache local.""" + + saida = [] + + # Dados do emitente + saida.append(self._serializar_emitente(objetos)) + + # Certificado Digital? XXX + + # Clientes + saida.append(self._serializar_clientes(objetos)) + + # Transportadoras + saida.append(self._serializar_transportadoras(objetos)) + + # Produtos + saida.append(self._serializar_produtos(objetos)) + + # Lote de Notas Fiscais + saida.append(self._serializar_notas_fiscais(objetos)) + + # FIXME + return '\n'.join(saida) + + def importar(self, objetos, origem): + """Cria as instancias do PyNFe a partir de arquivos XML no formato padrao da + SEFAZ e Receita Federal.""" + + raise Exception('Metodo nao implementado') + + def _serializar_emitente(self, objetos): + return '' + + def _serializar_clientes(self, objetos): + return '' + + def _serializar_transportadoras(self, objetos): + return '' + + def _serializar_produtos(self, objetos): + return '' + + def _serializar_notas_fiscais(self, objetos): + return '' diff --git a/tests/01-basico.txt b/tests/01-basico.txt index 3c7cddc..e7e3b70 100644 --- a/tests/01-basico.txt +++ b/tests/01-basico.txt @@ -1,7 +1,11 @@ TESTES BASICOS ============== - >>> import sets + >>> try: + ... set + ... except: + ... from sets import Set as set + A biblioteca deve fornecer uma colecao de utilitarios para consumir o webservice da NF-e. @@ -16,9 +20,9 @@ modelo: | MODELO DE ENTIDADES | --------------------------------------------------------------------- | | - | ------------ | - | | Entidade | | - | ------------ | + | ------------ -------------- | + | | Entidade |-------<>| FonteDados | | + | ------------ -------------- | | A | | | | | ----especializacao-------------------------- | @@ -41,16 +45,16 @@ modelo: | PROCESSAMENTO | -------------------------------------------------------------------------- | | - | ---------------- -------------- -------------------------------- | - | | InterfaceXML | | Assinatura | | Comunicacao | | - | ---------------- -------------- -------------------------------- | - | | exportar() | | assinar() | | transmitir() | | - | | importar() | -------------- | cancelar() | | - | ---------------- | situacao_nfe() | | - | ------------- | status_servico() | | - | -------------- | Validacao | | consultar_cadastro() | | - | | DANFE | ------------- | inutilizar_faixa_numeracao() | | - | -------------- | validar() | -------------------------------- | + | ------------------- -------------- -------------------------------- | + | | SerializacaoXML | | Assinatura | | Comunicacao | | + | ------------------- -------------- -------------------------------- | + | | exportar() | | assinar() | | transmitir() | | + | | importar() | -------------- | cancelar() | | + | ------------------- | situacao_nfe() | | + | ------------- | status_servico() | | + | -------------- | Validacao | | consultar_cadastro() | | + | | DANFE | ------------- | inutilizar_faixa_numeracao() | | + | -------------- | validar() | -------------------------------- | | | imprimir() | ------------- | | -------------- | | | @@ -62,23 +66,23 @@ Os pacotes da biblioteca sao: biblioteca, incluindo flags e funcoes genericas) >>> from pynfe import utils - >>> sets.Set([attr for attr in dir(utils) if not attr.startswith('__')]) == sets.Set(['flags']) + >>> set([attr for attr in dir(utils) if not attr.startswith('__')]) == set(['flags']) True - entidades (contem todas as entidades da biblioteca) >>> from pynfe import entidades - >>> sets.Set([attr for attr in dir(entidades) if not attr.startswith('__')]) == sets.Set([ + >>> set([attr for attr in dir(entidades) if not attr.startswith('__')]) == set([ ... 'Cliente', 'Emitente', 'LoteNotaFiscal', 'NotaFiscal', 'Produto', ... 'Transportadora', 'base', 'cliente', 'emitente', 'lotes', 'notafiscal', - ... 'produto', 'transportadora']) + ... 'produto', 'transportadora', 'fontes_dados', 'FonteDados']) True - processamento (contem todas as funcionalidades de processamento da biblioteca >>> from pynfe import processamento - >>> sets.Set([attr for attr in dir(processamento) if not attr.startswith('__')]) == sets.Set([ + >>> set([attr for attr in dir(processamento) if not attr.startswith('__')]) == set([ ... 'Assinatura', 'Comunicacao', 'DANFE', 'InterfaceXML', 'Validacao', ... 'assinatura', 'comunicacao', 'danfe', 'interfaces', 'validacao']) True diff --git a/tests/02-modelo-00-definicoes-gerais.txt b/tests/02-modelo-00-definicoes-gerais.txt index fe9fc10..b2d02b8 100644 --- a/tests/02-modelo-00-definicoes-gerais.txt +++ b/tests/02-modelo-00-definicoes-gerais.txt @@ -5,3 +5,105 @@ Modelo das entidades e como elas se relacionam. Nenhum dos campos deve permitir acentos e/ou cedilhas. +Todas as entidades devem referenciar uma Fonte de Dados, de forma a evitar +redundancia de dados (com o objetivo de melhorar o desempenho e possibilitar +o uso de cache de persistencia de dados serializados). + + >>> from pynfe.entidades import FonteDados + >>> fonte_dados = FonteDados() + +Nao eh da funcao do PyNFe efetuar a persistencia dos objetos, mas a classe +FonteDados deve facilitar esse processo ao software que for implementa-la. + + >>> hasattr(FonteDados, 'carregar_objetos') + True + + >>> from pynfe.entidades import Emitente + +Populando fonte de dados com objetos + + >>> bool(Emitente(cnpj='12.345.678/0001-90', _fonte_dados=fonte_dados)) + True + + >>> bool(Emitente(razao_social='JKL Calcados Ltda.', _fonte_dados=fonte_dados)) + True + + >>> bool(Emitente(razao_social='JKL Calcados Ltda.', _fonte_dados=fonte_dados)) + True + +O metodo carregar_objetos pode ser sobrecarregado para alterar o carregamento de +objetos da memoria para forcar mocking ou para carregar de camada persistente. +Ele sempre retorna uma lista de objetos, independente se vazia ou com qualquer +quantidade de objetos. + + >>> def carregar_objetos(self, **kwargs): + ... if kwargs.get('cnpj', None) == 'xxx': + ... return ['encontrado!'] + ... + ... return self.antigo_carregar_objetos(**kwargs) + +Substituindo metodo 'carregar_objetos' + + >>> fonte_dados.antigo_carregar_objetos = fonte_dados.carregar_objetos + + >>> import new + >>> fonte_dados.carregar_objetos = new.instancemethod(carregar_objetos, fonte_dados, FonteDados) + + >>> fonte_dados.obter_objeto(cnpj='xxx') + 'encontrado!' + +O metodo 'obter_objeto' retorna um unico objeto que atende aos atributos informados. +O argumento especial '_classe' eh utilizado para indicar que a classe da entidade eh +a atribuida a esse argumento. + + >>> emitente = fonte_dados.obter_objeto(cnpj='12.345.678/0001-90', _classe=Emitente) + + >>> isinstance(emitente, Emitente) + True + +Caso nenhum objeto seja encontrado, uma excecao deve ser levantada. + + >>> from pynfe.excecoes import NenhumObjetoEncontrado + + >>> try: + ... fonte_dados.obter_objeto(cnpj='98.765.432/0001-10', _classe=Emitente) + ... except NenhumObjetoEncontrado, e: + ... print e.message + Nenhum objeto foi encontrado! + +Caso mais de um objeto sejam encontrados, uma excecao deve ser levantada tambem. + + >>> from pynfe.excecoes import MuitosObjetosEncontrados + + >>> try: + ... fonte_dados.obter_objeto(razao_social='JKL Calcados Ltda.', _classe=Emitente) + ... except MuitosObjetosEncontrados, e: + ... print e.message + Muitos objetos foram encontrados! + +O metodo 'obter_lista' retorna uma lista de objetos, mesmo que vazia. + + >>> len(fonte_dados.obter_lista(razao_social='JKL Calcados Ltda.')) + 2 + + >>> len(fonte_dados.obter_lista(razao_social='Inexistente S/A')) + 0 + +Qualquer entidade que for instanciada deve ser acrescentada automaticamente a lista de +objetos da Fonte de Dados, atraves de um metodo especifico pra isso + + >>> conta_antes = fonte_dados.contar_objetos() + >>> emitente = Emitente(razao_social='Emitente Novo', _fonte_dados=fonte_dados) + >>> fonte_dados.contar_objetos() - conta_antes + 1 + +O contador de objetos retorna a quantidade de instancias que casem com os argumentos passados + + >>> fonte_dados.contar_objetos(_classe=Emitente, razao_social='Emitente Novo') + 1 + +Permitir tambem remover objetos (que por padrao remove apenas da lista da memoria e nao +eh persistente. + + >>> fonte_dados.remover_objeto(emitente) + diff --git a/tests/03-processamento.txt b/tests/03-processamento-00-definicoes-gerais.txt similarity index 98% rename from tests/03-processamento.txt rename to tests/03-processamento-00-definicoes-gerais.txt index bfa5492..a71eb2d 100644 --- a/tests/03-processamento.txt +++ b/tests/03-processamento-00-definicoes-gerais.txt @@ -1,16 +1,6 @@ PROCESSAMENTO ============= -Validar NF-e ------------- - -- Efetuar validacoes dos XSD no(s) XML(s) gerado(s) - -Assinar NF-e ------------- - -- Na hora de assinar, selecionar um Certificado Digital - Gerar arquivos XML ------------------ @@ -24,6 +14,16 @@ Gerar arquivos XML própria tag , conforme exemplo abaixo. - Cada documento XML deverá ter o seu namespace individual em seu elemento raiz. +Validar NF-e +------------ + +- Efetuar validacoes dos XSD no(s) XML(s) gerado(s) + +Assinar NF-e +------------ + +- Na hora de assinar, selecionar um Certificado Digital + Transmitir NF-e (ou lote de NF-e`s) ----------------------------------- @@ -49,8 +49,8 @@ Consulta da situação atual da NF-e Sincrona Consulta do status do serviço Sincrona Consulta cadastro Sincrona -Imprimir NF-e -------------- +Imprimir DANF-e +--------------- - Geracao baseada no Geraldo Reports - Gerar codigo de barras (padrao CODE-128C) diff --git a/tests/03-processamento-01-serializacao-xml.txt b/tests/03-processamento-01-serializacao-xml.txt new file mode 100644 index 0000000..3fda08e --- /dev/null +++ b/tests/03-processamento-01-serializacao-xml.txt @@ -0,0 +1,16 @@ +PROCESSAMENTO - SERIALIZACAO PARA XML +===================================== + +Gerar arquivos XML +------------------ + +- Gera os arquivos XML a partir dos dados das instancias da NF-e +- Quando gerados me lote, apenas o primeiro arquivo deve ter o cabecalho + padrao do XML 1.0 + - +- Namespace + - + - A declaração do namespace da assinatura digital deverá ser realizada na + própria tag , conforme exemplo abaixo. + - Cada documento XML deverá ter o seu namespace individual em seu elemento raiz. +