diff --git a/.gitignore b/.gitignore index 9b4ec02..11fc32a 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ dist/ build/ .eggs/ *.egg-info + + diff --git a/.travis.yml b/.travis.yml index 98af1bc..85761d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ install: - pip install coveralls script: - coverage run --source=sped setup.py test + - python test/fci_test.py deploy: provider: pypi user: ginx diff --git a/README.md b/README.md index 708f698..887ceae 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ O projeto está em fase inicial de desenvolvimento e **não deve** ser usado em | ECF | Funcional | | EFD-PIS/COFINS | Funcional | | EFD-ICMS/IPI | Funcional | +| FCI | Funcional | ### ECD @@ -75,3 +76,10 @@ Este módulo está funcional, com todos seus registros codificados, porém muito adequada, consultado tabelas externas por exemplo, ou validando corretamente todos os tamanhos de campos. Ele pode ser usado para gerar um arquivo digital, com validações de abertura e fechamento de bloco automaticamente. + +### FCI + +Este módulo está funcional, com todos seus registros codificados, porém muitos campos ainda não possuem uma validação +adequada, validando corretamente todos os tamanhos de campos. + +Ele pode ser usado para gerar um arquivo digital, com validações de abertura e fechamento de bloco automaticamente. diff --git a/setup.py b/setup.py index a178d7a..686703e 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,19 @@ def finalize_options(self): self.test_suite = True def run_tests(self): + # doctest import sys import pytest + errno = pytest.main(self.pytest_args) + + # unittest + import unittest + from test.fci_test import TestArquivoDigital + + suite = unittest.TestLoader().loadTestsFromTestCase(TestArquivoDigital) + unittest.TextTestRunner(verbosity=2).run(suite) + sys.exit(errno) diff --git a/sped/arquivos.py b/sped/arquivos.py index abe4982..37f062a 100644 --- a/sped/arquivos.py +++ b/sped/arquivos.py @@ -6,6 +6,7 @@ class ArquivoDigital(object): + registro_abertura = None registro_fechamento = None registros = None @@ -19,7 +20,7 @@ def __init__(self): def readfile(self, filename): with open(filename) as file: for line in [line.rstrip('\r\n') for line in file]: - self.read_registro(line) + self.read_registro(line.decode('utf8')) def read_registro(self, line): reg_id = line.split('|')[1] @@ -27,7 +28,7 @@ def read_registro(self, line): try: registro_class = getattr(self.__class__.registros, 'Registro' + reg_id) except AttributeError: - raise RuntimeError("Arquivo inválido para EFD - PIS/COFINS") + raise RuntimeError(u"Arquivo inválido para EFD - PIS/COFINS") registro = registro_class(line) diff --git a/sped/fci/__init__.py b/sped/fci/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/sped/fci/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/sped/fci/arquivos.py b/sped/fci/arquivos.py new file mode 100644 index 0000000..72439b8 --- /dev/null +++ b/sped/fci/arquivos.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +from ..arquivos import ArquivoDigital +from . import registros +from . import blocos +from .blocos import Bloco0 +from .blocos import Bloco5 +from .blocos import Bloco9 +from .registros import Registro0000 +from .registros import Registro9999 + + +class ArquivoDigital(ArquivoDigital): + registro_abertura = Registro0000 + registro_fechamento = Registro9999 + registros = registros + blocos = blocos + + def __init__(self): + super(ArquivoDigital, self).__init__() + self._blocos['0'] = Bloco0() + self._blocos['5'] = Bloco5() + self._blocos['9'] = Bloco9() + + utf = (u"0001|Texto em caracteres UTF-8: (dígrafo BR)'ção',(dígrafo " + u"espanhol-enhe)'ñ',(trema)'Ü',(ordinais)'ªº',(ligamento s+z a" + u"lemão)'ß'.") + self.read_registro(utf) + self.read_registro('|9900|0000|1') + self.read_registro('|9900|0010|1') + self.read_registro('|9900|5020|') + + def read_registro(self, line): + + # caso o usuario insira linha sem pip + pipe = '|' if line[0] != '|' else '' + line = pipe + line + reg_id = line.split('|')[1] + + try: + registro_class = \ + getattr(self.__class__.registros, 'Registro' + reg_id) + except AttributeError: + raise RuntimeError(u"Arquivo inválido para FCI") + + registro = registro_class(line) + if registro.__class__ == self.__class__.registro_abertura: + self._registro_abertura = registro + elif registro.__class__ == self.__class__.registro_fechamento: + self._registro_fechamento = registro + elif registro.__class__ == \ + self.__class__.blocos.Bloco0.registro_abertura: + self.blocos.Bloco0.abertura = registro + else: + bloco_id = reg_id[0] + bloco = self._blocos[bloco_id] + bloco.add(registro) + + # Contabiliza os registros 5020 + if reg_id == '5020': + registros_9 = self._blocos['9'].registros[3] + registros_9.valores[3] = \ + str((len(self._blocos['5'].registros)) - 2) + + def write_to(self, buffer): + + linha_abertura = self._registro_abertura.as_line()[1:] + buffer.write(linha_abertura + u'\r\n') + reg_count = 2 + for key in self._blocos.keys(): + bloco = self._blocos[key] + reg_count += len(bloco.registros) + for r in bloco.registros: + a = r.as_line() + a = a[1:] + buffer.write(a + u'\r\n') + + self._registro_fechamento[2] = reg_count + linha_fechamento = self._registro_fechamento.as_line()[1:] + buffer.write(linha_fechamento + u'\r\n') + + def readfile(self, filename): + + with open(filename) as arq: + for line in [line.rstrip('\r\n') for line in arq]: + if (line[:4] != '9900' and line[:4] != '0001' and line[:4] != '0990'): + self.read_registro(line.decode('utf-8-sig')) diff --git a/sped/fci/blocos.py b/sped/fci/blocos.py new file mode 100644 index 0000000..e787fe6 --- /dev/null +++ b/sped/fci/blocos.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +from ..blocos import Bloco + +from .registros import Registro0001 +from .registros import Registro0990 +from .registros import Registro5001 +from .registros import Registro5990 +from .registros import Registro9001 +from .registros import Registro9990 + + +class Bloco0(Bloco): + """ + Cabeçalho da FCI: Identificação do contribuinte + """ + registro_abertura = Registro0001 + registro_fechamento = Registro0990 + + @property + def abertura(self): + registro = self.__class__.registro_abertura() + return registro + + @property + def fechamento(self): + registro = self.__class__.registro_fechamento() + # Define a quantidade de registros + registro[2] = len(self._registros) + 3 + return registro + + def add(self, registro): + self._registros.append(registro) + + +class Bloco5(Bloco): + registro_abertura = Registro5001 + registro_fechamento = Registro5990 + + @property + def abertura(self): + registro = self.__class__.registro_abertura() + return registro + + +class Bloco9(Bloco): + """ + Controle e Encerramento do Arquivo Digital + """ + registro_abertura = Registro9001 + registro_fechamento = Registro9990 + + @property + def abertura(self): + registro = self.__class__.registro_abertura() + return registro diff --git a/sped/fci/registros.py b/sped/fci/registros.py new file mode 100644 index 0000000..5df534c --- /dev/null +++ b/sped/fci/registros.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- + +from ..campos import CampoFixo, CampoAlfanumerico, CampoNumerico +from ..registros import Registro +from ..erros import CampoError + + +class Registro0000(Registro): + campos = [ + CampoFixo(1, 'REG', '0000'), + CampoAlfanumerico(2, 'CNPJ_CONTRIBUINTE'), + CampoAlfanumerico(3, 'NOME_CONTRIBUINTE'), + CampoFixo(4, 'VERSAO_LEIAUTE', '1.0'), + CampoAlfanumerico(5, 'HASH CODE', obrigatorio=False, tamanho=47), + CampoAlfanumerico(6, 'DT_RECEPCAO_ARQUIVO', obrigatorio=False, + tamanho=20), + CampoAlfanumerico(7, 'COD_RECEPCAO_ARQUIVO', obrigatorio=False, + tamanho=36), + CampoAlfanumerico(8, 'DT_VALIDACAO_ARQUIVO', obrigatorio=False, + tamanho=20), + CampoAlfanumerico(9, 'IN_VALIDACAO_ARQUIVO', obrigatorio=False, + tamanho=20) + ] + + +class Registro0001(Registro): + campos = [ + CampoFixo(1, 'REG', "0001"), + CampoAlfanumerico(2, 'TEXTO_PADRAO_UTF8', obrigatorio=True) + ] + + +class Registro0010(Registro): + campos = [ + CampoFixo(1, 'REG', '0010'), + CampoAlfanumerico(2, 'CNPJ_CONTRIBUINTE', obrigatorio=True), + CampoAlfanumerico(3, 'NOME_RAZAO_SOCIAL', obrigatorio=True), + CampoAlfanumerico(4, 'INSCRICAO_ESTADUAL', obrigatorio=True), + CampoAlfanumerico(5, 'ENDERECO_ESTABELECIMENTO', obrigatorio=True), + CampoNumerico(6, 'CEP', obrigatorio=True), + CampoAlfanumerico(7, 'MUNICIPIO', obrigatorio=True), + CampoAlfanumerico(8, 'UF', obrigatorio=True) + ] + + +class Registro0990(Registro): + campos = [ + CampoFixo(1, 'REG', '0990'), + CampoNumerico(2, 'QUANTIDADE_LINHAS') + ] + + def __init__(self, line=None): + if not line: + self._valores = [''] * (len(self.campos) + 1) + for c in self.campos: + if isinstance(c, CampoFixo): + self._valores[c.indice] = c.valor + else: + self._valores = line.split('|') + for c in self.campos: + if isinstance(c, CampoFixo): + if self._valores[c.indice] != c.valor: + raise CampoError(self, c.nome) + + +class Registro5001(Registro): + campos = [ + CampoFixo(1, 'REG', '5001'), + ] + + def __init__(self, line=None): + if not line: + self._valores = [''] * (len(self.campos) + 1) + for c in self.campos: + if isinstance(c, CampoFixo): + self._valores[c.indice] = c.valor + else: + self._valores = line.split('|') + for c in self.campos: + if isinstance(c, CampoFixo): + if self._valores[c.indice] != c.valor: + raise CampoError(self, c.nome) + + +class Registro5020(Registro): + campos = [ + CampoFixo(1, 'REG', '5020'), + CampoAlfanumerico(2, 'NOME_MERCADORIA', obrigatorio=True, tamanho=255), + CampoNumerico(3, 'CODIGO_NCM', obrigatorio=True), + CampoAlfanumerico(4, 'CODIGO_MERCADORIA', obrigatorio=True, + tamanho=50), + CampoNumerico(5, 'CODIGO_GTIN'), + CampoAlfanumerico(6, 'UNIDADE_MERCADORIA', tamanho=6, + obrigatorio=True), + CampoNumerico(7, 'VALOR_SAIDA_MERCADORIA_INTERESTADUAL', precisao=2, + obrigatorio=True), + CampoNumerico(8, 'VALOR_PARCELA_IMPORTADA_EXTERIOR', precisao=2, + obrigatorio=True), + CampoNumerico(9, 'CONTEUDO_IMPORTACAO_CI', precisao=2, + obrigatorio=True), + CampoAlfanumerico(10, 'CODIGO_FCI', obrigatorio=False, tamanho=36), + CampoAlfanumerico(11, 'IN_VALIDACAO_FICHA', obrigatorio=False, + tamanho=20) + ] + + +class Registro5990(Registro): + campos = [ + CampoFixo(1, 'REG', '5990'), + CampoNumerico(2, 'QUANTIDADE_LINHAS'), + ] + + def __init__(self, line=None): + if not line: + self._valores = [''] * (len(self.campos) + 1) + for c in self.campos: + if isinstance(c, CampoFixo): + self._valores[c.indice] = c.valor + else: + self._valores = line.split('|') + for c in self.campos: + if isinstance(c, CampoFixo): + if self._valores[c.indice] != c.valor: + raise CampoError(self, c.nome) + + +class Registro9001(Registro): + campos = [ + CampoFixo(1, 'REG', '9001') + ] + + def __init__(self, line=None): + if not line: + self._valores = [''] * (len(self.campos) + 1) + for c in self.campos: + if isinstance(c, CampoFixo): + self._valores[c.indice] = c.valor + else: + self._valores = line.split('|') + for c in self.campos: + if isinstance(c, CampoFixo): + if self._valores[c.indice] != c.valor: + raise CampoError(self, c.nome) + + +class Registro9900(Registro): + campos = [ + CampoFixo(1, 'REG', '9900'), + CampoAlfanumerico(2, 'REG_SER_TOTALIZADO', tamanho=4), + CampoNumerico(3, 'QUANTIDADE_LINHAS_REGISTRO_ANTERIOR') + ] + + +class Registro9990(Registro): + campos = [ + CampoFixo(1, 'REG', '9990'), + CampoNumerico(2, 'QUANTIDADE_LINHAS') + ] + + def __init__(self, line=None): + if not line: + self._valores = [''] * (len(self.campos) + 1) + for c in self.campos: + if isinstance(c, CampoFixo): + self._valores[c.indice] = c.valor + else: + self._valores = line.split('|') + for c in self.campos: + if isinstance(c, CampoFixo): + if self._valores[c.indice] != c.valor: + raise CampoError(self, c.nome) + + +class Registro9999(Registro): + campos = [ + CampoFixo(1, 'REG', '9999'), + CampoNumerico(2, 'QUANTIDADE_LINHAS_ARQUIVO') + ] + + def __init__(self, line=None): + if not line: + self._valores = [''] * (len(self.campos) + 1) + for c in self.campos: + if isinstance(c, CampoFixo): + self._valores[c.indice] = c.valor + else: + self._valores = line.split('|') + for c in self.campos: + if isinstance(c, CampoFixo): + if self._valores[c.indice] != c.valor: + raise CampoError(self, c.nome) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/test/fci_test.py b/test/fci_test.py new file mode 100644 index 0000000..3b23ae5 --- /dev/null +++ b/test/fci_test.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +import unittest +import os +import sys + +# Necessário para que o arquivo de testes encontre +test_root = os.path.dirname(os.path.abspath(__file__)) +os.chdir(test_root) +sys.path.insert(0, os.path.dirname(test_root)) +sys.path.insert(0, test_root) + +from sped.fci import arquivos + + +class TestArquivoDigital(unittest.TestCase): + + def test_read_registro(self): + txt = u"""0000|11111111000191|EMPRESA TESTE|1.0 +0001|Texto em caracteres UTF-8: (dígrafo BR)'ção',(dígrafo espanhol-enhe)'ñ',(trema)'Ü',(ordinais)'ªº',(ligamento s+z alemão)'ß'. +0010|46377222000129|Contribuinte de Teste S/A|686001664111|Rua XV de novembro, 1.234|01506000|São João|SP +0990|4 +5001 +5020|Motor de pistão por ignição, cilindrada igual a 2.000 cm³ - R123-A4-5|84073490|12j8ai.5d0-ao4p|07123456789012|unid|9123,45|4567,89|50,07 +5020|Motor de pistão por ignição, cilindrada igual a 2.000 cm³ - R123-A4-5|84073490|12j8ai.5d0-ao4p|07123456789012|unid|9123,45|4567,89|50,07 +5020|Motor de pistão por ignição, cilindrada igual a 2.000 cm³ - R123-A4-5|84073490|12j8ai.5d0-ao4p|07123456789012|unid|9123,45|4567,89|50,07 +5990|5 +9001 +9900|0000|1 +9900|0010|1 +9900|5020|3 +9990|5 +9999|15 +""".replace('\n', '\r\n') + + # Permite validacao de string grandes + self.maxDiff = None + arq = arquivos.ArquivoDigital() + + arq.read_registro('0000|11111111000191|EMPRESA TESTE|1.0') + + arq.read_registro(u"|0001|Texto em caracteres UTF-8: (dígrafo BR)'ção'," + u"(dígrafo espanhol-enhe)'ñ',(trema)'Ü',(ordinais" + u")'ªº',(ligamento s+z alemão)'ß'.") + + arq.read_registro( u'|0010|46377222000129|Contribuinte de Teste ' + u'S/A|686001664111|Rua XV de novembro, 1.234|' + u'01506000|São João|SP') + + arq.read_registro(u'|5020|Motor de pistão por ignição, cilindrada ' + u'igual a 2.000 cm³ - R123-A4-5|84073490|' + u'12j8ai.5d0-ao4p|07123456789012|unid|9123,45|' + u'4567,89|50,07') + + arq.read_registro(u'|5020|Motor de pistão por ignição, cilindrada ' + u'igual a 2.000 cm³ - R123-A4-5|84073490|' + u'12j8ai.5d0-ao4p|07123456789012|unid|9123,45|' + u'4567,89|50,07') + + arq.read_registro(u'|5020|Motor de pistão por ignição, cilindrada ' + u'igual a 2.000 cm³ - R123-A4-5|84073490|' + u'12j8ai.5d0-ao4p|07123456789012|unid|9123,45|' + u'4567,89|50,07') + + self.assertEqual(txt, arq.getstring()) + +if __name__ == '__main__': + unittest.main()