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.
+