diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f20b403 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin +env +*png +__pycache__ diff --git a/32211207872718000117550010000217781877120005-nfe.xml b/32211207872718000117550010000217781877120005-nfe.xml deleted file mode 100644 index 0b01f56..0000000 --- a/32211207872718000117550010000217781877120005-nfe.xml +++ /dev/null @@ -1,2 +0,0 @@ -3287712000VENDA MERCAD.ADQ.TERCEIROS551217782021-12-01T19:03:21-03:002021-12-01T19:03:21-03:0011320130811511010MeLinux 6.307872718000117COMERCIAL S.R.DE ALIMENTOS LTDA-MESR ALIMENTOSR.ODONIA DA COSTA MACHADO TOLEDO56QUADRA 1 LOTE 5TIRADENTES3201308CARIACICAES291435291058BRASIL2733363026082384975317197072000173JEFERSON SIMAO DE OLIVEIRA - MERUA SAO JOAO49ITACIBA3201308CARIACICAES291502301058BRASIL279965021211082917825001378SEM GTINFILE MERLUZA ARGENTINA INTERF.CX 18 c/ 18 KG030474005102CX1503.82503.82SEM GTINCX1827.991220358.82207.4617.0035.279995301503.820.653.2701503.823.0015.110001957896481907388CUPIM MAGRO FRISA CX -+ 18KG0201300017084005405KG21.7129.12989406632.417896481907388KG21.7129.1298940610609995301632.410.654.1101632.413.0018.97207.4635.270.000.000.000.000.000.001136.230.000.000.000.000.000.007.3834.080.001136.23907872718000117COMERCIAL S.R.DE ALIMENTOS LTDA-ME082384975RUA ODONIA DA COSTA MACHADO TOLEDO, 41 QD 1 LOTE 5 TIRADENTECARIACICAES23DIVERSASDIVERSAS40.1060217781136.230.001136.230012022-12-15568.110022022-12-22568.12151136.23Cod.Dest: 7452 Fantasia: REST.ESPA.GRILL Faturista: IGOR REIS # Forma Pag: BOLETO # Vendedor: TACIANI (27)99735-5228 Num.Ped: 102667 # Doc.emitido por ME/EPP optante do SIMPLES NACIONAL; nao gera credito fiscal de IPI e ISS # RUA DA FEIRA # sem email cadastrado para envio do XML # # Confira no ato da entrega, evite reclamacoes posteriores19495981000113OTMA SOLUCOES LTDAcontato@otmasolucoes.com.br2721417943yU29u4Sva8be/dmxDzD7eyMIOaY=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 -1SVRS202111190951322112078727180001175500100002177818771200052021-12-01T19:03:22-03:00332210083442441yU29u4Sva8be/dmxDzD7eyMIOaY=100Autorizado o uso da NF-e \ No newline at end of file diff --git a/Database.py b/Database.py new file mode 100644 index 0000000..4cc4c1d --- /dev/null +++ b/Database.py @@ -0,0 +1,218 @@ +from multiprocessing.connection import Connection +import sqlite3 +from sqlite3 import Error +from NotaFiscal import NotaFiscal + +''' +Utilização do banco de dados: + Justificativa: Uma vez que um arquivo é lido ele é salvo no banco permitindo + acesso as informações relevante a aplicação em executações futuras do mesmo + 1. O programa deve listar valores e data de vencimento de boletos segundo um dado + indentificador (CPF OU CNPJ) de um fornecedor (emitente). + 2. o program deve listar nome, identificador (CPF ou CNPJ) e endereço dos + clientes de um dado fornecedor(emitente) + +''' + +class Database(): + def __init__(self, input_path: str, schema_path: str): + self.db_path : str = input_path + self.schema_path : str = schema_path + self.conn : Connection = self.create_connection() + self.cursor = self.conn.cursor() + + # Cria conexão com um banco de dados já existente ou cria + # um novo banco de dados atraves do esquema passado + def create_connection(self) -> Connection: + conn : Connection = None + try: + conn = sqlite3.connect(self.db_path) + with open(self.schema_path) as f: + conn.executescript(f.read()) + return conn + except Error as e: + print(e) + + return conn + + # Cria tabela no banco de dados ou caso sejam passos argumentos + # os mesmos são usados para prencher uma tabela existente. + def create_table(self, create_table_sql : str, args : str = None) -> None: + try: + if args == None: + self.cursor.execute(create_table_sql) + else: + self.cursor.execute(create_table_sql, args) + self.conn.commit() + except Error as e: + print(e) + + def create_emitente(self, emitente) -> None: + + sql = ''' INSERT INTO emitente(emitente_id, + nome) + VALUES(?,?) ''' + self.create_table(sql, (emitente['emitente_id'], + emitente['nome'])) + + def create_destinador(self, destinador) -> None: + sql = ''' INSERT INTO destinador(destinador_id, + nome) + VALUES(?,?) ''' + self.create_table(sql, (destinador['destinador_id'], + destinador['nome'])) + + def create_endereco_destinador(self, endereco) -> None: + sql = ''' INSERT INTO endereco_destinador(destinador_endereco_id, + destinador_id, + logradouro, + numero, + bairro, + municipio, + estado, + cep, + telefone) + VALUES(?,?,?,?,?,?,?,?,?) ''' + self.create_table(sql, (endereco['destinador_endereco_id'], + endereco['destinador_id'], + endereco['xLgr'], + endereco['nro'], + endereco['xBairro'], + endereco['xMun'], + endereco['UF'], + endereco['CEP'], + endereco['fone'] + )) + + def create_nota_fiscal(self, nota_fiscal): + sql = ''' INSERT INTO nota_fiscal(nota_fiscal_id, + emitente_id, + destinador_id, + valor_total) + VALUES(?,?,?,?) ''' + + self.create_table(sql, (nota_fiscal['nota_fiscal_id'], + nota_fiscal['emitente_id'], + nota_fiscal['destinador_id'], + nota_fiscal['valor_total'])) + + def create_duplicata(self, duplicata): + sql = ''' INSERT INTO duplicata(duplicata_id, + nota_fiscal_id, + data_vencimento, + valor_pago) + VALUES(?,?,?,?) ''' + self.create_table(sql, (duplicata['duplicata_id'], + duplicata['nota_fiscal_id'], + duplicata['data_vencimento'], + duplicata['valor_pago'])) + + # Checa se um dado identificador existe em uma dada tabela + def check_if_exists_in_bd(self, table : str, identifier : str) -> bool: + sql = "SELECT * FROM "+ table + " WHERE "+ table +"_id = ?" + self.cursor.execute(sql, (identifier,)) + + data = self.cursor.fetchone() + if data is None: + return True + else: + print('Emitente com identificador %s já existe no sistema'%(identifier)) + return False + + # Consulta nota fiscal de duplicatas de um dado emitente. + def consulta_nota_fiscal_e_duplicatas_de_um_emitente(self, identificador_emitente : str) -> dict: + + self.cursor.execute("SELECT * FROM nota_fiscal WHERE emitente_id = ?", + (identificador_emitente,)) + notas_fiscais=self.cursor.fetchall() + if notas_fiscais == None: + return None + + notas_fiscais_dict = [] + for nota_fiscal in notas_fiscais: + nota_fiscal_dict = {} + + nota_fiscal_dict['nota_fical_id'] = nota_fiscal[0] + nota_fiscal_dict['valor_total'] = nota_fiscal[3] + + self.cursor.execute("SELECT * FROM duplicata WHERE nota_fiscal_id = ?", + (nota_fiscal[0],)) + duplicatas=self.cursor.fetchall() + duplicatas_dict = [] + + for dup in duplicatas: + duplicata_dict = {} + duplicata_dict['duplicata_id'] = dup[0].split('NFe')[0] + duplicata_dict['data_vencimento'] = dup[2] + duplicata_dict['valor_pago'] = dup[3] + duplicatas_dict.append(duplicata_dict) + nota_fiscal_dict['duplicatas'] = duplicatas_dict + notas_fiscais_dict.append(nota_fiscal_dict) + + return notas_fiscais_dict + + + def consulta_clientes_de_um_emitente(self, identificador_emitente : str) -> dict: + + self.cursor.execute("SELECT * FROM nota_fiscal WHERE emitente_id = ?", + (identificador_emitente,)) + notas_fiscais=self.cursor.fetchall() + + if notas_fiscais == None: + return None + + destinadores_dict = [] + + for nota_fiscal in notas_fiscais: + self.cursor.execute("SELECT * FROM destinador WHERE destinador_id = ?", + (nota_fiscal[2],)) + destinadores=self.cursor.fetchall() + + for dest in destinadores: + + destinador_dict = {'destinador_id' : dest[0], 'nome':dest[1]} + destinadores_dict.append(destinador_dict) + + return destinadores_dict + + # Salva nota fiscal no banco de dados + def save_nota_fiscal_on_db(self, nota_fiscal : NotaFiscal): + + # Emitente + # Salva novo emitente (fornecedor) no banco de dados caso o mesmo + # não exista. + emitente_dict = {'emitente_id' : nota_fiscal.get_emit_identifier(), + 'nome' : nota_fiscal.get_emit_name()} + if self.check_if_exists_in_bd('emitente',emitente_dict['emitente_id']): + self.create_emitente(emitente_dict) + + # Destinador + # Salva novo destinador (cliente) no banco de dados caso o mesmo + # não exista + destinador_dict = {'destinador_id' : nota_fiscal.get_dest_identifier(), + 'nome' : nota_fiscal.get_dest_name()} + + if self.check_if_exists_in_bd("destinador",destinador_dict['destinador_id']): + self.create_destinador(destinador_dict) + + # Endereço do destinador + # Salva endereço relacionado a um novo destinador. + endereco_destinador = nota_fiscal.get_dest_enderDest() + self.create_endereco_destinador(endereco_destinador) + + # Nota fiscal + # Salva uma nova nota fiscal no banco caso a mesma não exista + nota_fiscal_dict = {'nota_fiscal_id' : nota_fiscal.get_nota_fiscal_id(), + 'emitente_id' : nota_fiscal.get_emit_identifier(), + 'destinador_id' : nota_fiscal.get_dest_identifier(), + 'valor_total' : nota_fiscal.get_nota_fiscal_valor_total()} + + if self.check_if_exists_in_bd("nota_fiscal",nota_fiscal_dict['nota_fiscal_id']): + self.create_nota_fiscal(nota_fiscal_dict) + + # Salva as duplicatas relacionadas a uma nota fiscal. + # Duplicata é parte da fatura pois a mesma pode ter mais de uma + # parcela. Duplicata também é chamada de boleto no contexto em + # que esse programa se aplica + for duplicata in nota_fiscal.get_faturas(): + self.create_duplicata(duplicata) \ No newline at end of file diff --git a/NFe-002-3103.xml b/NFe-002-3103.xml deleted file mode 100644 index 831d193..0000000 --- a/NFe-002-3103.xml +++ /dev/null @@ -1,11 +0,0 @@ -3100464032Vendas a prazo55231032019-04-10T17:24:03-02:002019-04-11T17:17:30-02:001131702061171101000106273476000182MECA Office Mobil. Eireli-MEMECA Office Mobil. Eireli-MEAV. MARCOS DE FREITAS COSTA1055DANIEL FOSECA3170206UberlandiaMG384003281058BRASIL34323855857022916720058125587387000155HLTS ENGENHARIA E CONSTRUCOES LTDARUA MACHADO DE ASSIS1324LIDICE3170206UberlandiaMG384000811058BRASIL34322359661702177134005400331SEM GTINCADEIRA GIRATORIA S/ BRACO ALMOFADADA PRETO940190905102UN2165.00000330.00SEM GTINUN2165.00000177.6801020808IMOBILIZADO IMOB - 2317 CADEIRA GIRATORIA S/ BRACO ALMOFADADA PRETO - IMOBILIZADO EQUIP. MOVEIS P/ CANTEIRO MATERIAIS INSTALACAO DE CANTEIRO.01228SEM GTINCADEIRA GIRATORIA C/ BRACO ALMOFADADA PRETO940190905102UN1215.00000215.00SEM GTINUN1215.00000150.6101020808IMOBILIZADO EQUIP/MOVEIS P/ CANTEIRO MATERIAIS INSTALACAO DE CANTEIRO01324SEM GTINBANQUETA ALTA 70CM PARA BALCAO ASSENTO 30 OU940190905102PC190.0000090.00SEM GTINPC190.00000121.190102080840CM NA COR PRETA - IMOBILIZADO EQUIP./MOVEIS P/ CANTEIRO MATERIAIS INSTALACAO DE CANTEIRO01644SEM GTINESTANTE ACO C/ 06 PRATELEIRAS 0,93X040X1,98M940690205102PC2234.00000468.00SEM GTINPC2234.00000145.8701020808(REFORCADA) AMAPA - IMOBILIZADO.0.000.000.000.000.000.000.000.001103.000.000.000.000.000.000.000.000.000.001103.00195.3506VOLUMEVARIAS0.0000.0000000031031103.000.001103.000012019-05-111103.00141103.000.00ORDEM DE FORNECIMENTO 36994 - 28DD - INFORMACOES COMPLEMENTARES a seguinte informacao. EMPRESA ENQUADRADA NO SIMPLES NACIONAL. NAO GERA CREDITO DE IPI/ISS. GERA CREDITO DE ICMS. Trib aprox R$: 54,84 Federal 140,51 Estadual Fonte: IBPT empresometro.com.br S3A6R4ngqVwH6QNCAHyRuI529RIAr7Nyk=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 - 1 - 14.2.26 - 31190406273476000182550020000031031004640327 - 2019-04-10T17:23:27-03:00 - 131193257591884 - ngqVwH6QNCAHyRuI529RIAr7Nyk= - 100 - Autorizado o uso da NF-e - - diff --git a/NotaFiscal.py b/NotaFiscal.py new file mode 100644 index 0000000..6c10edc --- /dev/null +++ b/NotaFiscal.py @@ -0,0 +1,179 @@ +# Biblioteca para trabalhar com XML +import xml.etree.ElementTree as ET + +class NotaFiscal: + def __init__(self, input_path: str): + self.xml_path : str = input_path + self.namespace: str = None + self.root : ET.Element = self.xml_to_ElementTree(input_path) + + def __str__(self) -> str: + str = ET.tostring(self.root, encoding='utf8').decode('utf8') + return str + + # Extrai ElementTree da XML + # ElementTree é uma árvore de elementos que representa do XML + # 'elemet' ou 'elemento' da árvore será utilizado para referênciar + # os galhoes ou folhas da árvore principal guardada em self.root + def xml_to_ElementTree(self,input_path: str) -> ET.Element: + try: + root_with_namespace : ET.Element = ET.iterparse(input_path) + except: + print('Erro no XML ', input_path ) + exit(1) + root : ET.Element = self.remove_namespace_from_nota_fiscal(root_with_namespace) + return root + + # Retorna ElementTree sem nameSpace + def remove_namespace_from_nota_fiscal(self, input: ET.Element) -> ET.Element: + + try: + for _, el in input: + prefix, has_namespace, postfix = el.tag.partition('}') + if has_namespace: + if postfix == 'NFe': + self.set_namespace_nota_fiscal(has_namespace) + el.tag = postfix # strip all namespaces + except: + print('Erro no XML ', self.xml_path ) + exit(1) + + root : ET.Element = input.root + return root + + # Guarda Namespace da nota fiscal + def set_namespace_nota_fiscal(self, namespace: str) -> None: + self.namespace = namespace + + # --------- Emitente --------- + + # Retorna nome do destinatário + def get_emit_name(self) -> str: + name = self.get_text_from_xml_tree_element('./NFe/infNFe/emit/xNome') + return name + + # Retorna cnpj do emissor da nota fiscal caso o + # mesmo seja uma pessoa jurídica + def get_emit_cnpj(self) -> str: + cnpj = self.get_text_from_xml_tree_element('./NFe/infNFe/emit/CNPJ') + return cnpj + + # Retorna cpf do emissor da nota fiscal caso o + # mesmo seja uma pessoa física + def get_emit_cpf(self) -> str: + cpf = self.get_text_from_xml_tree_element('./NFe/infNFe/emit/CPF') + return cpf + + # Retorna identificador do emissor + def get_emit_identifier(self) -> str: + cnpj = self.get_emit_cnpj() + if(cnpj): + return cnpj + else: + return self.get_emit_cpf() + + # Retorna endereço do emissor + def get_emit_enderEmit(self) -> dict: + enderEmit = self.get_dict_from_xml_tree_element('./NFe/infNFe/emit/enderEmit') + return enderEmit + + # --------- Destinador --------- + + # Retorna cnpj do destinatário da nota fiscal caso o + # mesmo seja uma pessoa jurídica + def get_dest_cnpj(self) -> str: + cnpj = self.get_text_from_xml_tree_element('./NFe/infNFe/dest/CNPJ') + return cnpj + + # Retorna cpf do destinatário da nota fiscal caso o + # mesmo seja uma pessoa física + def get_dest_cpf(self) -> str: + cnpj = self.get_text_from_xml_tree_element('./NFe/infNFe/dest/CPF') + return cnpj + + # Retorna identificador do destinatário + def get_dest_identifier(self) -> str: + cnpj = self.get_dest_cnpj() + if(cnpj): + return cnpj + else: + return self.get_dest_cpf() + + # Retorna nome do destinatário + def get_dest_name(self) -> str: + name = self.get_text_from_xml_tree_element('./NFe/infNFe/dest/xNome') + return name + + # --------- Endereço --------- + + # Retorna endereço do destinatário + def get_dest_enderDest(self) -> dict: + enderDest = self.get_dict_from_xml_tree_element('./NFe/infNFe/dest/enderDest') + enderDest['destinador_endereco_id'] = self.get_dest_cnpj() + enderDest['destinador_id'] = self.get_dest_cnpj() + + return enderDest + + # --------- Dados para identificação da nota fiscal --------- + def get_nota_fiscal_id(self) -> str: + xml_tree_element = self.root.find('./NFe/infNFe') + return xml_tree_element.attrib['Id'] + + def get_nota_fiscal_valor_total(self) -> str: + name = self.get_text_from_xml_tree_element('./NFe/infNFe/pag/detPag/vPag') + return name + + # --------- Fatura --------- + + # Retorna lista de faturas contidas em uma + # nota fiscal + def get_faturas(self) -> dict: + faturas = [] + for tree_element_fatura in self.root.findall('./NFe/infNFe/cobr/dup'): + fatura = {} + fatura['duplicata_id'] = tree_element_fatura.find('nDup').text + self.get_nota_fiscal_id() + fatura['nota_fiscal_id'] = self.get_nota_fiscal_id() + fatura['data_vencimento'] = tree_element_fatura.find('dVenc').text + fatura['valor_pago'] = tree_element_fatura.find('vDup').text + faturas.append(fatura) + return faturas + + # --------- Specific methods --------- + + # Retorna data de vencimento da nota fiscal + def get_data_vencimento(self) -> str: + data_vencimento = self.get_text_from_xml_tree_element('./NFe/infNFe/cobr/dup/dVenc') + return data_vencimento + + # Retorna valor total da nota fiscal + # (impostos + valor unitário*unidades de cada produto) + def get_valor_total(self) -> str: + valor_total_nota = self.get_text_from_xml_tree_element('./NFe/infNFe/total/ICMSTot/vNF') + return valor_total_nota + + # Exibe faturas da nota fiscal + # Listar os valores e data de Vencimento dos boletos + # presentes em um nota fiscal + def print_fatura(self) -> None: + faturas = self.get_faturas() + for fatura in faturas: + print("Valor: R$", fatura['vDup']) + print("Data de cencimento: ", fatura['dVenc']) + + # --------- UTILS --------- + + # Retorna o texto de um determinado elemento + def get_text_from_xml_tree_element(self, tree_path: str) -> str: + xml_tree_element = self.root.find(tree_path) + if xml_tree_element != None: + return xml_tree_element.text + return None + + # Retorna retorna dicionário equivalente + # a um elemento + def get_dict_from_xml_tree_element(self, tree_path: str) -> dict: + result_dict = {} + xml_tree_element = self.root.find(tree_path) + for chield in xml_tree_element: + result_dict[chield.tag] = chield.text + return result_dict diff --git a/README.md b/README.md index 817b8c7..9f217a7 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,53 @@ # Venha para Recomb +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=00KL_venhapararecomb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=00KL_venhapararecomb) -O desafio é desenvolver um programa que permita realizar as seguintes buscas: +## Documentação da solução -1) Listar os valores e data de Vencimento dos boletos presentes em um nota fiscal conforme o CPF ou CNPJ de um fornecedor. -2) Apresentar o nome, identificador (CPF ou CNPJ), endereço dos clientes de um fornecedor. +### Solução -**Escolha as tecnologias que você vai usar e tente montar uma solução completa para rodar a aplicação.** +A solução implementada recebe um ou mais arquivos .xml de notas fiscais em um página web. -Para enviar o resultado, basta realiazar um Fork deste repositório e abra um Pull Request, com seu nome. +Através de um serviço Flask, esses arquivos são enviado da página web para ter suas informações extraidas e salvas em um banco de dados. -É importante comentar que deve ser enviado apenas o código fonte. Não aceitaremos códigos compilados. +Por fim, é possível realizar consultas aos clientes e boletos de um dado fornecedor. +### Módulos -Por fim, o candidato deve atualizar o Readme.md com as seguintes informações: - - 1) Documentação da solução; - 2) Lista dos diferenciais implementados +A implentação da solução foi dividida em módulos: +- [Database](./Database.py): Classe que realiza comunicação com o banco. +- [NotaFiscal](./NotaFiscal.py): Classe que extrai e armazena em memória as informações de um arquivo XML. +- [App](./app.py): Arquivo com funções que criam interfaces entre as classes chave do sistema e os componentes HTML que exibem os resultados. +- [Schema](./schema.sql): Arquivo com schema do banco de dados. -## Avaliação +### Pastas +- [Static](./static): Pasta que contem os estilos CSS das páginas do sistema. +- [Templates](./templates/): Pasta que contem os arquivos HTML da página web. +- [Uploads](./uploads/): Pasta que armazena os quivos XML enviados pelo usuário do sistema. -O programa será avaliado levando em conta os seguintes critérios: -|Critério| Valor| -|-------|--------| -|Legibilidade do Código |10| -|Organização do Código|10| -|Documentação do código |10| -|Documentação da solução |10| -|Tratamento de Erros |10| -|Total| 50| +### Execução +Para preparar o ambiente execute o comando: +``` +make init_env +``` -A pontuação do candidato será a soma dos valores obtidos nos critérios acima. +Para executar o código após a preparação do ambiente execute: +``` +make all +``` -## Diferenciais +Durante a execução do algorítimo os dados utilizados serão guardados assim, caso o usuário deseje apagar todos os dados salvos basta rodar o comando: +``` +make clean_db_uploads +``` + +## Lista dos diferenciais implementados -O candidato pode aumentar a sua pontuação na seleção implementando um ou mais dos itens abaixo: |Item | Pontos Ganhos| |-----|--------------| |Criar um serviço com o problema |30| |Utilizar banco de dados |30| -|Implementar Clean Code |20| -|Implementar o padrão de programação da tecnologia escolhida |20| -|Qualidade de Código com SonarQube| 15| -|Implementar testes unitários |15| -|Implementar testes comportamentais | 15| -|Implementar integração com Travis |10| -|Implementar integração com Travis + SonarQube |10| -|Implementar usando Docker |5| -|Total | 170| - -A nota final do candidato será acrescido dos pontos referente ao item implementado corretamente. - -## Penalizações - -O candidato será desclassifiado nas seguintes situações: - -1) Submeter um solução que não funcione; -2) Não cumprir os critérios presentes no seção Avaliação; -3) Plágio; - - +|Qualidade de Código com SonarQube |15| +|Total | 75| +## Desafio +Descrito no [repositório original](https://github.com/recombX/venhapararecomb#readme) do desafio. diff --git a/__pycache__/NotaFiscal.cpython-38.pyc b/__pycache__/NotaFiscal.cpython-38.pyc new file mode 100644 index 0000000..256f5ae Binary files /dev/null and b/__pycache__/NotaFiscal.cpython-38.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..62a1f4c --- /dev/null +++ b/app.py @@ -0,0 +1,101 @@ +from flask import Flask, render_template, request, url_for, flash, redirect, jsonify +from flask_dropzone import Dropzone +from werkzeug.exceptions import abort +from typing import List +from NotaFiscal import NotaFiscal +from Database import Database +import os +import sqlite3 + +test_db : Database = None + +def get_db_connection(): + conn = sqlite3.connect('database.db') + conn.row_factory = sqlite3.Row + return conn + +def get_post(post_id): + conn = get_db_connection() + post = conn.execute('SELECT * FROM posts WHERE id = ?', + (post_id,)).fetchone() + conn.close() + if post is None: + abort(404) + return post + + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'your secret key' +basedir = os.path.abspath(os.path.dirname(__file__)) +app.config.update( + UPLOADED_PATH=os.path.join(basedir, 'uploads'), + DROPZONE_ALLOWED_FILE_CUSTOM = True, + DROPZONE_ALLOWED_FILE_TYPE='.xml', + DROPZONE_MAX_FILE_SIZE=3, + DROPZONE_MAX_FILES=30, +) + +app.run(debug=True) +dropzone = Dropzone(app) + +@app.route('/') +def index(): + + return render_template('index.html') + +# Busca no banco de d +@app.route('/search', methods=['GET', 'POST']) +def search(): + + # Recebe da página web o valor de um indentificador (CPF/CNPJ) + idetifier = request.args.get('identificador') + # Caso não receba nada não haverá busca alguma + if(idetifier == ''): + return render_template('index.html') + + # os.remove('database.db') + # Cria conexão com banco existente ou cria um novo banco + # Baseado no schema contido no aquivo schema.sql + test_db : Database = Database('database.db', 'schema.sql') + + # Consulta as notas fiscais e boletos de um dado emitente (fornecedor) + notas_fiscais_de_um_emitente = test_db.consulta_nota_fiscal_e_duplicatas_de_um_emitente(idetifier) + # Caso não encontre nenhuma nota fiscal não há o que procurar + if notas_fiscais_de_um_emitente == None: + return render_template('index.html') + + # Consulta todos os destinadores (clientes) de um fornecedor + clientes_de_um_emitente = test_db.consulta_clientes_de_um_emitente(idetifier) + + return render_template('index.html', notas_fiscais_de_um_emitente=notas_fiscais_de_um_emitente, clientes_de_um_emitente = clientes_de_um_emitente) + +# Permite que os arquivos colocados na área de upload sejam salvos na pasta uploads +@app.route('/', methods=['POST', 'GET']) +def upload_xml_files(): + if request.method == 'POST': + f = request.files.get('file') + file_path = os.path.join(app.config['UPLOADED_PATH'], f.filename) + f.save(file_path) + + # os.remove('database.db') + test_db : Database = Database('database.db', 'schema.sql') + + # vetor das notas fiscais lidas na entrada + vet_nota_fiscal : List[NotaFiscal] = [] + + # Lê arquivos xml e guarda eles em objetos + # do tipo NotaFiscal + for file in os.listdir("uploads"): + if file.endswith(".xml"): + nota_fiscal = NotaFiscal("uploads/" + file) + vet_nota_fiscal.append(nota_fiscal) + + # Percorre as notas fiscais guardadas + for nota_fiscal in vet_nota_fiscal: + # Salva uma nota fiscal no banco de dados + test_db.save_nota_fiscal_on_db(nota_fiscal) + + return render_template('index.html') + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/makefile b/makefile new file mode 100644 index 0000000..173b016 --- /dev/null +++ b/makefile @@ -0,0 +1,19 @@ +export FLASK_ENV := development + +all: + echo FLASK_ENV: $$FLASK_ENV + flask run + +clean_db_uploads: + rm -rf database.db uploads/* + +init_env: + python3 -m venv env + . env/bin/activate + pip install -r requirements.txt + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..287aaf7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +click==8.1.2 +Flask==2.1.1 +Flask-Dropzone==1.6.0 +importlib-metadata==4.11.3 +itsdangerous==2.1.2 +Jinja2==3.1.1 +MarkupSafe==2.1.1 +Werkzeug==2.1.0 +zipp==3.7.0 \ No newline at end of file diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..bfd6fec --- /dev/null +++ b/schema.sql @@ -0,0 +1,49 @@ +DROP TABLE IF EXISTS posts; + +CREATE TABLE posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title TEXT NOT NULL, + content TEXT NOT NULL +); + + +CREATE TABLE IF NOT EXISTS emitente ( + emitente_id text PRIMARY KEY, + nome text NOT NULL +); + +CREATE TABLE IF NOT EXISTS destinador ( + destinador_id text PRIMARY KEY, + nome text NOT NULL +); + +CREATE TABLE IF NOT EXISTS endereco_destinador ( + destinador_endereco_id text PRIMARY KEY, + destinador_id text, + logradouro text NOT NULL, + numero text NOT NULL, + bairro text NOT NULL, + municipio text NOT NULL, + estado text NOT NULL, + cep text NOT NULL, + telefone text NOT NULL, + FOREIGN KEY (destinador_id) REFERENCES destinador (destinador_id) +); + +CREATE TABLE IF NOT EXISTS nota_fiscal ( + nota_fiscal_id text PRIMARY KEY, + emitente_id text NOT NULL, + destinador_id text NOT NULL, + valor_total text NOT NULL, + FOREIGN KEY (emitente_id) REFERENCES emitente (emitente_id), + FOREIGN KEY (destinador_id) REFERENCES destinador (destinador_id) +); + +CREATE TABLE IF NOT EXISTS duplicata ( + duplicata_id text PRIMARY KEY, + nota_fiscal_id text NOT NULL, + data_vencimento text NOT NULL, + valor_pago text NOT NULL, + FOREIGN KEY (nota_fiscal_id) REFERENCES nota_fiscal (nota_fiscal_id) +); \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..110b3ef --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,6 @@ +h1 { + border: 2px #eee solid; + color: brown; + text-align: center; + padding: 10px; +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..abbb1ee --- /dev/null +++ b/templates/base.html @@ -0,0 +1,41 @@ + + + + + + + + + + {{ dropzone.load_css() }} + {{ dropzone.style('border: 2px dashed #0087F7; margin: 3%; min-height: 100px;') }} + {% block title %} {% endblock %} + + + +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %} {% endblock %} +
+ + + + + + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..42e4b0c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,82 @@ +{% extends 'base.html' %} + +{% block content %} +

{% block title %} Bem vindo ao leitor de notas fiscais {% endblock %}

+ +

+ Este site busca boletos e clientes baseado no identificador (CPF ou CNPJ) de um fornecedor. +

+ +

+
Digite o identificador de um forncedor:
+ + +
+ +
+

Clientes:

+
+ + + + + + + + + + + {% for cliente in clientes_de_um_emitente %} + + + + + + {% endfor %} + +
Tabela: Clientes
#destinador_idnome
1{{ cliente.destinador_id }}{{ cliente.nome }}
+
+
+ +
+ +

Notas fiscais:

+ {% for nota_fiscal in notas_fiscais_de_um_emitente %} +
+
{{'infNFe: ' + nota_fiscal.nota_fical_id + ' Valor total a pagar: ' + nota_fiscal.valor_total}}
+ {% if nota_fiscal.duplicatas == []%} +
Esta nota fiscal não possui boletos.
+ {% else %} + + + + + + + + + + + {% for duplicata in nota_fiscal.duplicatas %} + + + + + + {% endfor %} + +
Tabela: Notas fiscais
Boleto n°data_vencimentovalor_pago
1{{ duplicata.data_vencimento }}{{ duplicata.valor_pago }}
+ {% endif %} +
+ {% endfor %} +
+ + {{ dropzone.create(action='upload_xml_files') }} + {{ dropzone.load_js() }} + {{ dropzone.config(default_message='Arreste os arquivos .xml das notas fiscais aqui.') }} + {# You can get the success response from server like this: #} + {#{ dropzone.config(custom_options="success: function(file, response){console.log(response);}") }#} + + +{% endblock %}