diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ca2b787 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..e5e2cd6 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,21 @@ +name: Documentation +on: + push: + branches: + - main +jobs: + change-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Generate documentation + run: | + pip install pdoc3 + pip install flask + PYTHONPATH=src pdoc --html src/ --force --output-dir ./docs + - name: Commit and push changes + uses: devops-infra/action-commit-push@v0.9.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + commit_message: Documentation update diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..9f87619 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,18 @@ +name: Quality +on: + push: + branches: + - main +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df6c0b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +env/ +db/database.db +__pycache__/ +src/__pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1b3a7a7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# set base image (host OS) +FROM python:3.10 + +# set the working directory in the container +WORKDIR /recomb + +# copy the dependencies file to the working directory +COPY requirements.txt . + +# install dependencies +RUN pip install -r requirements.txt + +# copy the content of the local schema to the working directory +COPY db/schema.sql db/schema.sql + +# copy the content of the local src directory to the working directory +COPY src/ src/ + +# command to run on container start +CMD [ "python", "./src/service.py" ] diff --git a/README.md b/README.md index 817b8c7..1084932 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,45 @@ # Venha para Recomb -O desafio é desenvolver um programa que permita realizar as seguintes buscas: +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=LKhoe_venhapararecomb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=LKhoe_venhapararecomb) -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. +## Documentação da solução -**Escolha as tecnologias que você vai usar e tente montar uma solução completa para rodar a aplicação.** +### Solução -Para enviar o resultado, basta realiazar um Fork deste repositório e abra um Pull Request, com seu nome. +O Código desenvolvido cria um serviço Flask que permite o upload de arquivos de NFe em formato `.xml` e realiza o parsing em cada um deles. -É importante comentar que deve ser enviado apenas o código fonte. Não aceitaremos códigos compilados. +A partir do parsing as informações de Cliente, Fornecedor e Boletos de uma NFe são extraídos e armazenados em um banco de dados. -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 +Também no serviço Flask é possível consultar informações de um Fornecedor a partir de seu identificador (CPF ou CNPJ). -## Avaliação +As informações informadas são aquelas especificadas no desafio: +1. Nome, CPF ou CNPJ e Endereço dos Clientes que compram desse Fornecedor; +2. Boletos emitidos por esse Fornecedor; -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| +### Código -A pontuação do candidato será a soma dos valores obtidos nos critérios acima. +O código foi desenvolvido em módulos, onde cada módulo realiza operações em uma área específica. +- [Database](./src/database.py): Responsável pela conexão de manipulação do banco de dados. +- [Parsing](./src/parsing.py): Responsável por reconhecer dos arquivos de entrada e extrair as informações dele. +- [Models](./src/models.py): Define as classes que representam os objetos que serão manipulados. -## Diferenciais +Além dos módulos foi desenvolvido um serviço em Flask para facilitar o uso. +- [Service](./src/service.py): Serviço em Flask responsável pela aplicação web. + +A documentação de cada uma das funções dos módulos foi gerada pelo [pdoc3](https://pypi.org/project/pdoc3/), através dos comentários feitos no código. +Para consultar a documentação em um formato amigável: +- Clone o repositório ```git clone https://github.com/LKhoe/venhapararecomb.git``` +- Na pasta `docs/` abra o arquivo `index.html` com algum navegador. + +### Execução + +```bash +docker build -t recomb . +docker run -it -p 5000:5000 recomb +``` + +## 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| @@ -40,23 +47,9 @@ O candidato pode aumentar a sua pontuação na seleção implementando um ou mai |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; - - +|Total | 120| +## Desafio +Descrito no [repositório original](https://github.com/recombX/venhapararecomb#readme) do desafio. \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..69ae57e --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,34 @@ +DROP TABLE IF EXISTS fornecedores; +DROP TABLE IF EXISTS clientes; +DROP TABLE IF EXISTS boletos; +DROP TABLE IF EXISTS notas_fiscais; + +CREATE TABLE fornecedores ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + identificador TEXT NOT NULL UNIQUE +); + +CREATE TABLE clientes ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + identificador TEXT NOT NULL UNIQUE, + nome TEXT NOT NULL, + endereco TEXT NOT NULL +); + +CREATE TABLE notas_fiscais ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + chave_acesso TEXT NOT NULL UNIQUE, + fornecedor_id INTEGER, + cliente_id INTEGER, + FOREIGN KEY (fornecedor_id) REFERENCES fornecedores(_id), + FOREIGN KEY (cliente_id) REFERENCES clientes(_id) +); + +CREATE TABLE boletos ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + valor REAL NOT NULL, + vencimento TIMESTAMP NOT NULL, + nota_fiscal_id INTEGER, + FOREIGN KEY (nota_fiscal_id) REFERENCES notas_fiscais(_id) +); + diff --git a/docs/src/database.html b/docs/src/database.html new file mode 100644 index 0000000..d404546 --- /dev/null +++ b/docs/src/database.html @@ -0,0 +1,675 @@ + + + + + + +src.database API documentation + + + + + + + + + + + +
+
+
+

Module src.database

+
+
+
+ +Expand source code + +
import sqlite3
+from contextlib import closing
+from datetime import datetime
+from models import Fornecedor, Cliente, NotaFiscal
+
+def create_database(db_file_path, db_schema_path):
+    """
+    Creates a database file from a schema file.
+
+    Args:
+        db_file_path (str): The path to the database file.
+        db_schema_path (str): The path to the schema file.
+    
+    Returns:
+        None
+    """
+    with closing(sqlite3.connect(db_file_path)) as connection:
+        with open(db_schema_path) as f:
+            connection.executescript(f.read())
+        connection.commit()
+
+def create_if_not_exist(connection, cursor, should_commit, table_name, columns, primary_key, primary_attribute, item_tuple):
+    """
+    Creates a new item in the database if it doesn't exist.
+    
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        table_name (str): The name of the table to insert the item into.
+        columns (list): The list of columns to insert the item into.
+        primary_key (str): The name of the primary key column.
+        primary_attribute (str): The value of the primary key attribute.
+        item_tuple (tuple): The tuple of values to insert into the table.
+        
+    Returns:
+        int: The id of the item. 
+    """
+    result = cursor.execute(f"SELECT * FROM {table_name} WHERE {primary_key} = ?", (primary_attribute,)).fetchone()
+    if result is None:
+        cursor.execute(f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({', '.join(['?']*len(columns))})",
+            item_tuple
+        )
+        _id = cursor.lastrowid
+        if should_commit:
+            connection.commit()
+    else:
+        _id = result[0]
+    return _id
+
+def create_many(connection, cursor, should_commit, table_name, columns, items_list):
+    """
+    Creates many items in the database.
+
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        table_name (str): The name of the table to insert the item into.
+        columns (list): The list of columns to insert the item into.
+        items_list (list): The list of items to insert into the table.
+
+    Returns:
+        None
+    """
+    cursor.executemany(f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({', '.join(['?']*len(columns))})",
+        items_list
+    )
+    if should_commit:
+        connection.commit()
+
+def create_fornecedor(connection, cursor, should_commit, fornecedor):
+    """
+    Creates a new fornecedor in the database if it not exists.
+
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        fornecedor (Fornecedor): The fornecedor to insert into the database.
+
+    Returns:
+        int: The id of the fornecedor.
+    """
+    return create_if_not_exist(connection, cursor, should_commit, 
+        'fornecedores', ['identificador'], 'identificador', 
+        fornecedor.identificador, (fornecedor.identificador,))
+
+def create_cliente(connection, cursor, should_commit, cliente):
+    """
+    Creates a new cliente in the database if it not exists.
+    
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        cliente (Cliente): The cliente to insert into the database.
+
+    Returns:
+        int: The id of the cliente.
+    """
+    return create_if_not_exist(connection, cursor, should_commit, 
+        'clientes', ['identificador', 'nome', 'endereco'], 'identificador', 
+        cliente.identificador, (cliente.identificador, cliente.nome, cliente.endereco))
+
+def create_boletos(connection, cursor, should_commit, boletos):
+    """
+    Creates many boletos in the database.
+
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        boletos (list): The list of boletos to insert into the database.
+    
+    Returns:
+        None
+    """
+    notas_fiscais_id = {str(boleto[0]) for boleto in boletos}
+    results = cursor.execute(f"SELECT * FROM boletos WHERE nota_fiscal_id IN ({','.join(notas_fiscais_id)})").fetchall()
+    results = map(lambda x: (x[3], x[1], datetime.strptime(x[2], '%Y-%m-%d %H:%M:%S')), results)
+    boletos = list(set(boletos) - set(results))
+    return create_many(connection, cursor, should_commit, 
+        'boletos', ['nota_fiscal_id', 'valor', 'vencimento'], 
+        boletos)
+
+def create_nota_fiscal(db_file_path, nota_fiscal):
+    """
+    Creates a new nota fiscal in the database if it not exists.
+
+    Args:
+        db_file_path (str): The path to the database file.
+        nota_fiscal (NotaFiscal): The nota fiscal to insert into the database.
+
+    Returns:
+        int: The id of the nota fiscal.
+    """
+    with closing(sqlite3.connect(db_file_path)) as connection:
+        with closing(connection.cursor()) as cursor:
+            fornecedor_id = create_fornecedor(connection, cursor, False, nota_fiscal.fornecedor)
+            cliente_id = create_cliente(connection, cursor, False, nota_fiscal.cliente)
+            nota_fiscal_id = create_if_not_exist(connection, cursor, False, 
+                'notas_fiscais', ['chave_acesso', 'fornecedor_id', 'cliente_id'], 'chave_acesso',
+                nota_fiscal.chave_acesso, (nota_fiscal.chave_acesso, fornecedor_id, cliente_id))
+            create_boletos(connection, cursor, False, [(nota_fiscal_id, boleto['valor'], boleto['vencimento']) for boleto in nota_fiscal.boletos])
+            connection.commit()
+
+def query1(db_file_path, fornecedor_identificador):
+    """
+    Queries the database for the list of all boletos issued to a fornecedor.
+
+    Args:
+        db_file_path (str): The path to the database file.
+        fornecedor_identificador (str): The fornecedor's identificador.
+
+    Returns:
+        list: The list of boletos issued to the fornecedor.
+    """
+    with closing(sqlite3.connect(db_file_path)) as connection:
+        with closing(connection.cursor()) as cursor:
+            return [{'valor': float(val), 'vencimento': datetime.strptime(ven, '%Y-%m-%d %H:%M:%S')} for _, val, ven in
+                    cursor.execute("""
+                    SELECT DISTINCT boletos.nota_fiscal_id, boletos.valor, boletos.vencimento
+                    FROM boletos
+                    INNER JOIN notas_fiscais ON boletos.nota_fiscal_id = notas_fiscais._id
+                    INNER JOIN fornecedores ON notas_fiscais.fornecedor_id = fornecedores._id
+                    WHERE fornecedores.identificador = ?;
+                """, (fornecedor_identificador, )).fetchall()
+            ]
+
+def query2(db_file_path, fornecedor_identificador):
+    """
+    Queries the database for the list of all clientes of a fornecedor.
+
+    Args:
+        db_file_path (str): The path to the database file.
+        fornecedor_identificador (str): The fornecedor's identificador.
+
+    Returns:
+        list: The list of clientes of the fornecedor.
+    """
+    with closing(sqlite3.connect(db_file_path)) as connection:
+        with closing(connection.cursor()) as cursor:
+            return [Cliente(i, n, e) for i, n, e in 
+                cursor.execute("""
+                    SELECT DISTINCT clientes.identificador, clientes.nome, clientes.endereco
+                    FROM clientes
+                    INNER JOIN notas_fiscais ON clientes._id = notas_fiscais.cliente_id
+                    INNER JOIN fornecedores ON notas_fiscais.fornecedor_id = fornecedores._id
+                    WHERE fornecedores.identificador = ?;
+                """, (fornecedor_identificador, )).fetchall()
+            ]
+
+
+
+
+
+
+
+

Functions

+
+
+def create_boletos(connection, cursor, should_commit, boletos) +
+
+

Creates many boletos in the database.

+

Args

+
+
connection : sqlite3.Connection
+
The connection to the database.
+
cursor : sqlite3.Cursor
+
The cursor to the database.
+
should_commit : bool
+
Whether or not to commit the changes to the database.
+
boletos : list
+
The list of boletos to insert into the database.
+
+

Returns

+

None

+
+ +Expand source code + +
def create_boletos(connection, cursor, should_commit, boletos):
+    """
+    Creates many boletos in the database.
+
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        boletos (list): The list of boletos to insert into the database.
+    
+    Returns:
+        None
+    """
+    notas_fiscais_id = {str(boleto[0]) for boleto in boletos}
+    results = cursor.execute(f"SELECT * FROM boletos WHERE nota_fiscal_id IN ({','.join(notas_fiscais_id)})").fetchall()
+    results = map(lambda x: (x[3], x[1], datetime.strptime(x[2], '%Y-%m-%d %H:%M:%S')), results)
+    boletos = list(set(boletos) - set(results))
+    return create_many(connection, cursor, should_commit, 
+        'boletos', ['nota_fiscal_id', 'valor', 'vencimento'], 
+        boletos)
+
+
+
+def create_cliente(connection, cursor, should_commit, cliente) +
+
+

Creates a new cliente in the database if it not exists.

+

Args

+
+
connection : sqlite3.Connection
+
The connection to the database.
+
cursor : sqlite3.Cursor
+
The cursor to the database.
+
should_commit : bool
+
Whether or not to commit the changes to the database.
+
cliente : Cliente
+
The cliente to insert into the database.
+
+

Returns

+
+
int
+
The id of the cliente.
+
+
+ +Expand source code + +
def create_cliente(connection, cursor, should_commit, cliente):
+    """
+    Creates a new cliente in the database if it not exists.
+    
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        cliente (Cliente): The cliente to insert into the database.
+
+    Returns:
+        int: The id of the cliente.
+    """
+    return create_if_not_exist(connection, cursor, should_commit, 
+        'clientes', ['identificador', 'nome', 'endereco'], 'identificador', 
+        cliente.identificador, (cliente.identificador, cliente.nome, cliente.endereco))
+
+
+
+def create_database(db_file_path, db_schema_path) +
+
+

Creates a database file from a schema file.

+

Args

+
+
db_file_path : str
+
The path to the database file.
+
db_schema_path : str
+
The path to the schema file.
+
+

Returns

+

None

+
+ +Expand source code + +
def create_database(db_file_path, db_schema_path):
+    """
+    Creates a database file from a schema file.
+
+    Args:
+        db_file_path (str): The path to the database file.
+        db_schema_path (str): The path to the schema file.
+    
+    Returns:
+        None
+    """
+    with closing(sqlite3.connect(db_file_path)) as connection:
+        with open(db_schema_path) as f:
+            connection.executescript(f.read())
+        connection.commit()
+
+
+
+def create_fornecedor(connection, cursor, should_commit, fornecedor) +
+
+

Creates a new fornecedor in the database if it not exists.

+

Args

+
+
connection : sqlite3.Connection
+
The connection to the database.
+
cursor : sqlite3.Cursor
+
The cursor to the database.
+
should_commit : bool
+
Whether or not to commit the changes to the database.
+
fornecedor : Fornecedor
+
The fornecedor to insert into the database.
+
+

Returns

+
+
int
+
The id of the fornecedor.
+
+
+ +Expand source code + +
def create_fornecedor(connection, cursor, should_commit, fornecedor):
+    """
+    Creates a new fornecedor in the database if it not exists.
+
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        fornecedor (Fornecedor): The fornecedor to insert into the database.
+
+    Returns:
+        int: The id of the fornecedor.
+    """
+    return create_if_not_exist(connection, cursor, should_commit, 
+        'fornecedores', ['identificador'], 'identificador', 
+        fornecedor.identificador, (fornecedor.identificador,))
+
+
+
+def create_if_not_exist(connection, cursor, should_commit, table_name, columns, primary_key, primary_attribute, item_tuple) +
+
+

Creates a new item in the database if it doesn't exist.

+

Args

+
+
connection : sqlite3.Connection
+
The connection to the database.
+
cursor : sqlite3.Cursor
+
The cursor to the database.
+
should_commit : bool
+
Whether or not to commit the changes to the database.
+
table_name : str
+
The name of the table to insert the item into.
+
columns : list
+
The list of columns to insert the item into.
+
primary_key : str
+
The name of the primary key column.
+
primary_attribute : str
+
The value of the primary key attribute.
+
item_tuple : tuple
+
The tuple of values to insert into the table.
+
+

Returns

+
+
int
+
The id of the item.
+
+
+ +Expand source code + +
def create_if_not_exist(connection, cursor, should_commit, table_name, columns, primary_key, primary_attribute, item_tuple):
+    """
+    Creates a new item in the database if it doesn't exist.
+    
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        table_name (str): The name of the table to insert the item into.
+        columns (list): The list of columns to insert the item into.
+        primary_key (str): The name of the primary key column.
+        primary_attribute (str): The value of the primary key attribute.
+        item_tuple (tuple): The tuple of values to insert into the table.
+        
+    Returns:
+        int: The id of the item. 
+    """
+    result = cursor.execute(f"SELECT * FROM {table_name} WHERE {primary_key} = ?", (primary_attribute,)).fetchone()
+    if result is None:
+        cursor.execute(f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({', '.join(['?']*len(columns))})",
+            item_tuple
+        )
+        _id = cursor.lastrowid
+        if should_commit:
+            connection.commit()
+    else:
+        _id = result[0]
+    return _id
+
+
+
+def create_many(connection, cursor, should_commit, table_name, columns, items_list) +
+
+

Creates many items in the database.

+

Args

+
+
connection : sqlite3.Connection
+
The connection to the database.
+
cursor : sqlite3.Cursor
+
The cursor to the database.
+
should_commit : bool
+
Whether or not to commit the changes to the database.
+
table_name : str
+
The name of the table to insert the item into.
+
columns : list
+
The list of columns to insert the item into.
+
items_list : list
+
The list of items to insert into the table.
+
+

Returns

+

None

+
+ +Expand source code + +
def create_many(connection, cursor, should_commit, table_name, columns, items_list):
+    """
+    Creates many items in the database.
+
+    Args:
+        connection (sqlite3.Connection): The connection to the database.
+        cursor (sqlite3.Cursor): The cursor to the database.
+        should_commit (bool): Whether or not to commit the changes to the database.
+        table_name (str): The name of the table to insert the item into.
+        columns (list): The list of columns to insert the item into.
+        items_list (list): The list of items to insert into the table.
+
+    Returns:
+        None
+    """
+    cursor.executemany(f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({', '.join(['?']*len(columns))})",
+        items_list
+    )
+    if should_commit:
+        connection.commit()
+
+
+
+def create_nota_fiscal(db_file_path, nota_fiscal) +
+
+

Creates a new nota fiscal in the database if it not exists.

+

Args

+
+
db_file_path : str
+
The path to the database file.
+
nota_fiscal : NotaFiscal
+
The nota fiscal to insert into the database.
+
+

Returns

+
+
int
+
The id of the nota fiscal.
+
+
+ +Expand source code + +
def create_nota_fiscal(db_file_path, nota_fiscal):
+    """
+    Creates a new nota fiscal in the database if it not exists.
+
+    Args:
+        db_file_path (str): The path to the database file.
+        nota_fiscal (NotaFiscal): The nota fiscal to insert into the database.
+
+    Returns:
+        int: The id of the nota fiscal.
+    """
+    with closing(sqlite3.connect(db_file_path)) as connection:
+        with closing(connection.cursor()) as cursor:
+            fornecedor_id = create_fornecedor(connection, cursor, False, nota_fiscal.fornecedor)
+            cliente_id = create_cliente(connection, cursor, False, nota_fiscal.cliente)
+            nota_fiscal_id = create_if_not_exist(connection, cursor, False, 
+                'notas_fiscais', ['chave_acesso', 'fornecedor_id', 'cliente_id'], 'chave_acesso',
+                nota_fiscal.chave_acesso, (nota_fiscal.chave_acesso, fornecedor_id, cliente_id))
+            create_boletos(connection, cursor, False, [(nota_fiscal_id, boleto['valor'], boleto['vencimento']) for boleto in nota_fiscal.boletos])
+            connection.commit()
+
+
+
+def query1(db_file_path, fornecedor_identificador) +
+
+

Queries the database for the list of all boletos issued to a fornecedor.

+

Args

+
+
db_file_path : str
+
The path to the database file.
+
fornecedor_identificador : str
+
The fornecedor's identificador.
+
+

Returns

+
+
list
+
The list of boletos issued to the fornecedor.
+
+
+ +Expand source code + +
def query1(db_file_path, fornecedor_identificador):
+    """
+    Queries the database for the list of all boletos issued to a fornecedor.
+
+    Args:
+        db_file_path (str): The path to the database file.
+        fornecedor_identificador (str): The fornecedor's identificador.
+
+    Returns:
+        list: The list of boletos issued to the fornecedor.
+    """
+    with closing(sqlite3.connect(db_file_path)) as connection:
+        with closing(connection.cursor()) as cursor:
+            return [{'valor': float(val), 'vencimento': datetime.strptime(ven, '%Y-%m-%d %H:%M:%S')} for _, val, ven in
+                    cursor.execute("""
+                    SELECT DISTINCT boletos.nota_fiscal_id, boletos.valor, boletos.vencimento
+                    FROM boletos
+                    INNER JOIN notas_fiscais ON boletos.nota_fiscal_id = notas_fiscais._id
+                    INNER JOIN fornecedores ON notas_fiscais.fornecedor_id = fornecedores._id
+                    WHERE fornecedores.identificador = ?;
+                """, (fornecedor_identificador, )).fetchall()
+            ]
+
+
+
+def query2(db_file_path, fornecedor_identificador) +
+
+

Queries the database for the list of all clientes of a fornecedor.

+

Args

+
+
db_file_path : str
+
The path to the database file.
+
fornecedor_identificador : str
+
The fornecedor's identificador.
+
+

Returns

+
+
list
+
The list of clientes of the fornecedor.
+
+
+ +Expand source code + +
def query2(db_file_path, fornecedor_identificador):
+    """
+    Queries the database for the list of all clientes of a fornecedor.
+
+    Args:
+        db_file_path (str): The path to the database file.
+        fornecedor_identificador (str): The fornecedor's identificador.
+
+    Returns:
+        list: The list of clientes of the fornecedor.
+    """
+    with closing(sqlite3.connect(db_file_path)) as connection:
+        with closing(connection.cursor()) as cursor:
+            return [Cliente(i, n, e) for i, n, e in 
+                cursor.execute("""
+                    SELECT DISTINCT clientes.identificador, clientes.nome, clientes.endereco
+                    FROM clientes
+                    INNER JOIN notas_fiscais ON clientes._id = notas_fiscais.cliente_id
+                    INNER JOIN fornecedores ON notas_fiscais.fornecedor_id = fornecedores._id
+                    WHERE fornecedores.identificador = ?;
+                """, (fornecedor_identificador, )).fetchall()
+            ]
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/src/index.html b/docs/src/index.html new file mode 100644 index 0000000..4ccf7f1 --- /dev/null +++ b/docs/src/index.html @@ -0,0 +1,75 @@ + + + + + + +src API documentation + + + + + + + + + + + +
+
+
+

Namespace src

+
+
+
+
+

Sub-modules

+
+
src.database
+
+
+
+
src.models
+
+
+
+
src.parsing
+
+
+
+
src.service
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/src/models.html b/docs/src/models.html new file mode 100644 index 0000000..d365d82 --- /dev/null +++ b/docs/src/models.html @@ -0,0 +1,209 @@ + + + + + + +src.models API documentation + + + + + + + + + + + +
+
+
+

Module src.models

+
+
+
+ +Expand source code + +
from datetime import datetime
+
+class Fornecedor():
+    """
+    Class that represents a fornecedor.
+
+    Attributes:
+        identificador (str): The fornecedor's identifier.
+    """
+    def __init__(self, identificador):
+        self.identificador = identificador
+
+class Cliente():
+    """
+    Class that represents a cliente.
+
+    Attributes:
+        identificador (str): The cliente's identifier.
+        nome (str): The cliente's name.
+        endereco (str): The cliente's address.
+    """
+    def __init__(self, identificador, nome, endereco):
+        self.identificador = identificador
+        self.nome = nome
+        self.endereco = endereco
+
+class NotaFiscal():
+    """
+    Class that represents a nota fiscal.
+
+    Attributes:
+        fornecedor (Fornecedor): The fornecedor of the nota fiscal.
+        cliente (Cliente): The cliente of the nota fiscal.
+        boletos (list): The list of boletos of the nota fiscal.
+    """
+    def __init__(self, chave_acesso, fornecedor, cliente, boletos):
+        self.chave_acesso = chave_acesso
+        self.fornecedor = fornecedor
+        self.cliente = cliente
+        self.boletos = boletos
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Cliente +(identificador, nome, endereco) +
+
+

Class that represents a cliente.

+

Attributes

+
+
identificador : str
+
The cliente's identifier.
+
nome : str
+
The cliente's name.
+
endereco : str
+
The cliente's address.
+
+
+ +Expand source code + +
class Cliente():
+    """
+    Class that represents a cliente.
+
+    Attributes:
+        identificador (str): The cliente's identifier.
+        nome (str): The cliente's name.
+        endereco (str): The cliente's address.
+    """
+    def __init__(self, identificador, nome, endereco):
+        self.identificador = identificador
+        self.nome = nome
+        self.endereco = endereco
+
+
+
+class Fornecedor +(identificador) +
+
+

Class that represents a fornecedor.

+

Attributes

+
+
identificador : str
+
The fornecedor's identifier.
+
+
+ +Expand source code + +
class Fornecedor():
+    """
+    Class that represents a fornecedor.
+
+    Attributes:
+        identificador (str): The fornecedor's identifier.
+    """
+    def __init__(self, identificador):
+        self.identificador = identificador
+
+
+
+class NotaFiscal +(chave_acesso, fornecedor, cliente, boletos) +
+
+

Class that represents a nota fiscal.

+

Attributes

+
+
fornecedor : Fornecedor
+
The fornecedor of the nota fiscal.
+
cliente : Cliente
+
The cliente of the nota fiscal.
+
boletos : list
+
The list of boletos of the nota fiscal.
+
+
+ +Expand source code + +
class NotaFiscal():
+    """
+    Class that represents a nota fiscal.
+
+    Attributes:
+        fornecedor (Fornecedor): The fornecedor of the nota fiscal.
+        cliente (Cliente): The cliente of the nota fiscal.
+        boletos (list): The list of boletos of the nota fiscal.
+    """
+    def __init__(self, chave_acesso, fornecedor, cliente, boletos):
+        self.chave_acesso = chave_acesso
+        self.fornecedor = fornecedor
+        self.cliente = cliente
+        self.boletos = boletos
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/src/parsing.html b/docs/src/parsing.html new file mode 100644 index 0000000..dd976c8 --- /dev/null +++ b/docs/src/parsing.html @@ -0,0 +1,290 @@ + + + + + + +src.parsing API documentation + + + + + + + + + + + +
+
+
+

Module src.parsing

+
+
+
+ +Expand source code + +
import xml.etree.ElementTree as ET
+import re
+from models import Fornecedor, Cliente, NotaFiscal
+from datetime import datetime
+
+def parse_fornecedor(fornecedor_xml, namespace=''):
+    """
+    Parses a fornecedor from an XML element.
+
+    Args:
+        fornecedor_xml (xml.etree.ElementTree.Element): The XML element to parse.
+        namespace (str): The namespace of the XML element.
+    
+    Returns:
+        Fornecedor: The parsed fornecedor.
+    """
+    if (cnpj := fornecedor_xml.find(f'{namespace}CNPJ')) is not None:
+        identificador = cnpj.text
+    else:
+        identificador = fornecedor_xml.find(f'{namespace}CPF').text
+
+    return Fornecedor(identificador)
+
+def parse_cliente(cliente_xml, namespace=''):
+    """"
+    Parses a cliente from an XML element.
+    
+    Args:
+        cliente_xml (xml.etree.ElementTree.Element): The XML element to parse.
+        namespace (str): The namespace of the XML element.
+
+    Returns:
+        Cliente: The parsed cliente.
+    """
+    if (cnpj := cliente_xml.find(f'{namespace}CNPJ')) is not None:
+        identificador = cnpj.text
+    else:
+        identificador = cliente_xml.find(f'{namespace}CPF').text
+    nome = cliente_xml.find(f'{namespace}xNome').text
+
+    endereco_xml = cliente_xml.find(f'{namespace}enderDest')
+    logradouro = endereco_xml.find(f'{namespace}xLgr').text
+    numero = endereco_xml.find(f'{namespace}nro').text
+    municipio = endereco_xml.find(f'{namespace}xMun').text
+    unidade_federativa = endereco_xml.find(f'{namespace}UF').text
+    endereco = f'{logradouro}, Nº {numero}, {municipio} - {unidade_federativa}'
+
+    return Cliente(identificador, nome, endereco)
+
+def parse_nota_fiscal(file_content):
+    """
+    Parses a nota fiscal from an XML file.
+
+    Args:
+        file_content (str): The XML file content.
+
+    Returns:
+        NotaFiscal/None: The parsed nota fiscal or None if the file is not a nota fiscal.
+    """
+    try:
+        root_xml = ET.fromstring(file_content)
+        nfe_xml = root_xml[0] # NFe
+        inf_nfe_xml = nfe_xml[0] # infNFe
+        namespace = regex.group(0) if (regex := re.match(r'\{.*\}', root_xml.tag)) else '' # xmlns
+
+        fornecedor = parse_fornecedor(inf_nfe_xml.find(f'{namespace}emit'), namespace)
+        cliente = parse_cliente(inf_nfe_xml.find(f'{namespace}dest'), namespace)
+
+        cobranca_xml = inf_nfe_xml.find(f'{namespace}cobr')
+        duplicata_xml = cobranca_xml.findall(f'{namespace}dup')
+        boletos = [
+            {
+                'valor': float(dup_xml.find(f'{namespace}vDup').text), 
+                'vencimento': datetime.strptime(dup_xml.find(f'{namespace}dVenc').text, '%Y-%m-%d')
+            } 
+            for dup_xml in duplicata_xml
+        ]
+
+        return NotaFiscal(inf_nfe_xml.attrib['Id'][3:], fornecedor, cliente, boletos)
+
+    except Exception:
+        return None
+
+
+
+
+
+
+
+

Functions

+
+
+def parse_cliente(cliente_xml, namespace='') +
+
+

" +Parses a cliente from an XML element.

+

Args

+
+
cliente_xml : xml.etree.ElementTree.Element
+
The XML element to parse.
+
namespace : str
+
The namespace of the XML element.
+
+

Returns

+
+
Cliente
+
The parsed cliente.
+
+
+ +Expand source code + +
def parse_cliente(cliente_xml, namespace=''):
+    """"
+    Parses a cliente from an XML element.
+    
+    Args:
+        cliente_xml (xml.etree.ElementTree.Element): The XML element to parse.
+        namespace (str): The namespace of the XML element.
+
+    Returns:
+        Cliente: The parsed cliente.
+    """
+    if (cnpj := cliente_xml.find(f'{namespace}CNPJ')) is not None:
+        identificador = cnpj.text
+    else:
+        identificador = cliente_xml.find(f'{namespace}CPF').text
+    nome = cliente_xml.find(f'{namespace}xNome').text
+
+    endereco_xml = cliente_xml.find(f'{namespace}enderDest')
+    logradouro = endereco_xml.find(f'{namespace}xLgr').text
+    numero = endereco_xml.find(f'{namespace}nro').text
+    municipio = endereco_xml.find(f'{namespace}xMun').text
+    unidade_federativa = endereco_xml.find(f'{namespace}UF').text
+    endereco = f'{logradouro}, Nº {numero}, {municipio} - {unidade_federativa}'
+
+    return Cliente(identificador, nome, endereco)
+
+
+
+def parse_fornecedor(fornecedor_xml, namespace='') +
+
+

Parses a fornecedor from an XML element.

+

Args

+
+
fornecedor_xml : xml.etree.ElementTree.Element
+
The XML element to parse.
+
namespace : str
+
The namespace of the XML element.
+
+

Returns

+
+
Fornecedor
+
The parsed fornecedor.
+
+
+ +Expand source code + +
def parse_fornecedor(fornecedor_xml, namespace=''):
+    """
+    Parses a fornecedor from an XML element.
+
+    Args:
+        fornecedor_xml (xml.etree.ElementTree.Element): The XML element to parse.
+        namespace (str): The namespace of the XML element.
+    
+    Returns:
+        Fornecedor: The parsed fornecedor.
+    """
+    if (cnpj := fornecedor_xml.find(f'{namespace}CNPJ')) is not None:
+        identificador = cnpj.text
+    else:
+        identificador = fornecedor_xml.find(f'{namespace}CPF').text
+
+    return Fornecedor(identificador)
+
+
+
+def parse_nota_fiscal(file_content) +
+
+

Parses a nota fiscal from an XML file.

+

Args

+
+
file_content : str
+
The XML file content.
+
+

Returns

+

NotaFiscal/None: The parsed nota fiscal or None if the file is not a nota fiscal.

+
+ +Expand source code + +
def parse_nota_fiscal(file_content):
+    """
+    Parses a nota fiscal from an XML file.
+
+    Args:
+        file_content (str): The XML file content.
+
+    Returns:
+        NotaFiscal/None: The parsed nota fiscal or None if the file is not a nota fiscal.
+    """
+    try:
+        root_xml = ET.fromstring(file_content)
+        nfe_xml = root_xml[0] # NFe
+        inf_nfe_xml = nfe_xml[0] # infNFe
+        namespace = regex.group(0) if (regex := re.match(r'\{.*\}', root_xml.tag)) else '' # xmlns
+
+        fornecedor = parse_fornecedor(inf_nfe_xml.find(f'{namespace}emit'), namespace)
+        cliente = parse_cliente(inf_nfe_xml.find(f'{namespace}dest'), namespace)
+
+        cobranca_xml = inf_nfe_xml.find(f'{namespace}cobr')
+        duplicata_xml = cobranca_xml.findall(f'{namespace}dup')
+        boletos = [
+            {
+                'valor': float(dup_xml.find(f'{namespace}vDup').text), 
+                'vencimento': datetime.strptime(dup_xml.find(f'{namespace}dVenc').text, '%Y-%m-%d')
+            } 
+            for dup_xml in duplicata_xml
+        ]
+
+        return NotaFiscal(inf_nfe_xml.attrib['Id'][3:], fornecedor, cliente, boletos)
+
+    except Exception:
+        return None
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/src/service.html b/docs/src/service.html new file mode 100644 index 0000000..f0d9be5 --- /dev/null +++ b/docs/src/service.html @@ -0,0 +1,261 @@ + + + + + + +src.service API documentation + + + + + + + + + + + +
+
+
+

Module src.service

+
+
+
+ +Expand source code + +
from flask import Flask, redirect, render_template, request, url_for
+from werkzeug.utils import secure_filename
+import re
+from parsing import parse_nota_fiscal
+from database import create_database as create_database_original, \
+    create_nota_fiscal as create_nota_fiscal_original, \
+    query1 as query1_original, \
+    query2 as query2_original
+
+# Setup functions for local database
+DB_FILE_PATH = 'db/database.db'
+DB_SCHEMA_PATH = 'db/schema.sql'
+create_database = lambda: create_database_original(DB_FILE_PATH, DB_SCHEMA_PATH)
+create_nota_fiscal = lambda x: create_nota_fiscal_original(DB_FILE_PATH, x)
+query1 = lambda x: query1_original(DB_FILE_PATH, x)
+query2 = lambda x: query2_original(DB_FILE_PATH, x)
+
+# Setup Flask app
+app = Flask(__name__)
+
+@app.route('/')
+def page_1():
+   return render_template('1.html')
+
+@app.route('/2', methods = ['GET', 'POST'])
+def page_2():
+    if request.method != 'POST':
+        # To avoid breaking the flow of application, we will redirect to the first page
+        return redirect(url_for('page_1'))
+
+    # Get the list of files from the request
+    files = request.files.getlist("file")
+    for file in files:
+        if file.filename == '':
+            continue
+        nf = parse_nota_fiscal(file.read()) # Parse the file
+        if nf is not None:
+            create_nota_fiscal(nf) # Create the nota fiscal in database
+
+    return render_template('2.html')
+
+@app.route('/3', methods = ['GET', 'POST'])
+def page_3():
+    if request.method != 'POST':
+        # To avoid breaking the flow of application, we will redirect to the first page
+        return redirect(url_for('page_1'))
+    # Get the identificador from the request without the following characters . - /
+    identificador = re.sub(r'[\.\-\/]', '', request.form.get('identificador', '')) 
+    boletos = query1(identificador) # Get all boletos from a fornecedor
+    clientes = query2(identificador) # Get all clientes related to a fornecedor
+
+    return render_template('3.html', clientes=clientes, boletos=boletos)
+
+@app.template_filter()
+def format_datetime(value):
+    return value.strftime('%d/%m/%Y')
+
+if __name__ == '__main__':
+    try:
+        create_database() # Create the database
+    except Exception:
+        print("Não foi possível criar o banco de dados")
+    app.run("0.0.0.0", port=5000, debug = True) # Run the app
+
+
+
+
+
+
+
+

Functions

+
+
+def create_database() +
+
+
+
+ +Expand source code + +
create_database = lambda: create_database_original(DB_FILE_PATH, DB_SCHEMA_PATH)
+
+
+
+def create_nota_fiscal(x) +
+
+
+
+ +Expand source code + +
create_nota_fiscal = lambda x: create_nota_fiscal_original(DB_FILE_PATH, x)
+
+
+
+def format_datetime(value) +
+
+
+
+ +Expand source code + +
@app.template_filter()
+def format_datetime(value):
+    return value.strftime('%d/%m/%Y')
+
+
+
+def page_1() +
+
+
+
+ +Expand source code + +
@app.route('/')
+def page_1():
+   return render_template('1.html')
+
+
+
+def page_2() +
+
+
+
+ +Expand source code + +
@app.route('/2', methods = ['GET', 'POST'])
+def page_2():
+    if request.method != 'POST':
+        # To avoid breaking the flow of application, we will redirect to the first page
+        return redirect(url_for('page_1'))
+
+    # Get the list of files from the request
+    files = request.files.getlist("file")
+    for file in files:
+        if file.filename == '':
+            continue
+        nf = parse_nota_fiscal(file.read()) # Parse the file
+        if nf is not None:
+            create_nota_fiscal(nf) # Create the nota fiscal in database
+
+    return render_template('2.html')
+
+
+
+def page_3() +
+
+
+
+ +Expand source code + +
@app.route('/3', methods = ['GET', 'POST'])
+def page_3():
+    if request.method != 'POST':
+        # To avoid breaking the flow of application, we will redirect to the first page
+        return redirect(url_for('page_1'))
+    # Get the identificador from the request without the following characters . - /
+    identificador = re.sub(r'[\.\-\/]', '', request.form.get('identificador', '')) 
+    boletos = query1(identificador) # Get all boletos from a fornecedor
+    clientes = query2(identificador) # Get all clientes related to a fornecedor
+
+    return render_template('3.html', clientes=clientes, boletos=boletos)
+
+
+
+def query1(x) +
+
+
+
+ +Expand source code + +
query1 = lambda x: query1_original(DB_FILE_PATH, x)
+
+
+
+def query2(x) +
+
+
+
+ +Expand source code + +
query2 = lambda x: query2_original(DB_FILE_PATH, x)
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/32211207872718000117550010000217781877120005-nfe.xml b/in_xml/32211207872718000117550010000217781877120005-nfe.xml similarity index 100% rename from 32211207872718000117550010000217781877120005-nfe.xml rename to in_xml/32211207872718000117550010000217781877120005-nfe.xml diff --git a/NFe-002-3103.xml b/in_xml/NFe-002-3103.xml similarity index 100% rename from NFe-002-3103.xml rename to in_xml/NFe-002-3103.xml diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9e1538e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +click==8.1.0 +Flask==2.1.0 +itsdangerous==2.1.2 +Jinja2==3.1.1 +Mako==1.2.0 +Markdown==3.3.6 +MarkupSafe==2.1.1 +pdoc3==0.10.0 +Werkzeug==2.1.0 diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..75b3a11 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,12 @@ +sonar.projectKey=LKhoe_venhapararecomb +sonar.organization=lkhoe + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=venhapararecomb +#sonar.projectVersion=1.0 + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 \ No newline at end of file diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..b011422 --- /dev/null +++ b/src/database.py @@ -0,0 +1,192 @@ +import sqlite3 +from contextlib import closing +from datetime import datetime +from models import Fornecedor, Cliente, NotaFiscal + +def create_database(db_file_path, db_schema_path): + """ + Creates a database file from a schema file. + + Args: + db_file_path (str): The path to the database file. + db_schema_path (str): The path to the schema file. + + Returns: + None + """ + with closing(sqlite3.connect(db_file_path)) as connection: + with open(db_schema_path) as f: + connection.executescript(f.read()) + connection.commit() + +def create_if_not_exist(connection, cursor, should_commit, table_name, columns, primary_key, primary_attribute, item_tuple): + """ + Creates a new item in the database if it doesn't exist. + + Args: + connection (sqlite3.Connection): The connection to the database. + cursor (sqlite3.Cursor): The cursor to the database. + should_commit (bool): Whether or not to commit the changes to the database. + table_name (str): The name of the table to insert the item into. + columns (list): The list of columns to insert the item into. + primary_key (str): The name of the primary key column. + primary_attribute (str): The value of the primary key attribute. + item_tuple (tuple): The tuple of values to insert into the table. + + Returns: + int: The id of the item. + """ + result = cursor.execute(f"SELECT * FROM {table_name} WHERE {primary_key} = ?", (primary_attribute,)).fetchone() + if result is None: + cursor.execute(f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({', '.join(['?']*len(columns))})", + item_tuple + ) + _id = cursor.lastrowid + if should_commit: + connection.commit() + else: + _id = result[0] + return _id + +def create_many(connection, cursor, should_commit, table_name, columns, items_list): + """ + Creates many items in the database. + + Args: + connection (sqlite3.Connection): The connection to the database. + cursor (sqlite3.Cursor): The cursor to the database. + should_commit (bool): Whether or not to commit the changes to the database. + table_name (str): The name of the table to insert the item into. + columns (list): The list of columns to insert the item into. + items_list (list): The list of items to insert into the table. + + Returns: + None + """ + cursor.executemany(f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({', '.join(['?']*len(columns))})", + items_list + ) + if should_commit: + connection.commit() + +def create_fornecedor(connection, cursor, should_commit, fornecedor): + """ + Creates a new fornecedor in the database if it not exists. + + Args: + connection (sqlite3.Connection): The connection to the database. + cursor (sqlite3.Cursor): The cursor to the database. + should_commit (bool): Whether or not to commit the changes to the database. + fornecedor (Fornecedor): The fornecedor to insert into the database. + + Returns: + int: The id of the fornecedor. + """ + return create_if_not_exist(connection, cursor, should_commit, + 'fornecedores', ['identificador'], 'identificador', + fornecedor.identificador, (fornecedor.identificador,)) + +def create_cliente(connection, cursor, should_commit, cliente): + """ + Creates a new cliente in the database if it not exists. + + Args: + connection (sqlite3.Connection): The connection to the database. + cursor (sqlite3.Cursor): The cursor to the database. + should_commit (bool): Whether or not to commit the changes to the database. + cliente (Cliente): The cliente to insert into the database. + + Returns: + int: The id of the cliente. + """ + return create_if_not_exist(connection, cursor, should_commit, + 'clientes', ['identificador', 'nome', 'endereco'], 'identificador', + cliente.identificador, (cliente.identificador, cliente.nome, cliente.endereco)) + +def create_boletos(connection, cursor, should_commit, boletos): + """ + Creates many boletos in the database. + + Args: + connection (sqlite3.Connection): The connection to the database. + cursor (sqlite3.Cursor): The cursor to the database. + should_commit (bool): Whether or not to commit the changes to the database. + boletos (list): The list of boletos to insert into the database. + + Returns: + None + """ + notas_fiscais_id = {str(boleto[0]) for boleto in boletos} + results = cursor.execute(f"SELECT * FROM boletos WHERE nota_fiscal_id IN ({','.join(notas_fiscais_id)})").fetchall() + results = map(lambda x: (x[3], x[1], datetime.strptime(x[2], '%Y-%m-%d %H:%M:%S')), results) + boletos = list(set(boletos) - set(results)) + return create_many(connection, cursor, should_commit, + 'boletos', ['nota_fiscal_id', 'valor', 'vencimento'], + boletos) + +def create_nota_fiscal(db_file_path, nota_fiscal): + """ + Creates a new nota fiscal in the database if it not exists. + + Args: + db_file_path (str): The path to the database file. + nota_fiscal (NotaFiscal): The nota fiscal to insert into the database. + + Returns: + int: The id of the nota fiscal. + """ + with closing(sqlite3.connect(db_file_path)) as connection: + with closing(connection.cursor()) as cursor: + fornecedor_id = create_fornecedor(connection, cursor, False, nota_fiscal.fornecedor) + cliente_id = create_cliente(connection, cursor, False, nota_fiscal.cliente) + nota_fiscal_id = create_if_not_exist(connection, cursor, False, + 'notas_fiscais', ['chave_acesso', 'fornecedor_id', 'cliente_id'], 'chave_acesso', + nota_fiscal.chave_acesso, (nota_fiscal.chave_acesso, fornecedor_id, cliente_id)) + create_boletos(connection, cursor, False, [(nota_fiscal_id, boleto['valor'], boleto['vencimento']) for boleto in nota_fiscal.boletos]) + connection.commit() + +def query1(db_file_path, fornecedor_identificador): + """ + Queries the database for the list of all boletos issued to a fornecedor. + + Args: + db_file_path (str): The path to the database file. + fornecedor_identificador (str): The fornecedor's identificador. + + Returns: + list: The list of boletos issued to the fornecedor. + """ + with closing(sqlite3.connect(db_file_path)) as connection: + with closing(connection.cursor()) as cursor: + return [{'valor': float(val), 'vencimento': datetime.strptime(ven, '%Y-%m-%d %H:%M:%S')} for _, val, ven in + cursor.execute(""" + SELECT DISTINCT boletos.nota_fiscal_id, boletos.valor, boletos.vencimento + FROM boletos + INNER JOIN notas_fiscais ON boletos.nota_fiscal_id = notas_fiscais._id + INNER JOIN fornecedores ON notas_fiscais.fornecedor_id = fornecedores._id + WHERE fornecedores.identificador = ?; + """, (fornecedor_identificador, )).fetchall() + ] + +def query2(db_file_path, fornecedor_identificador): + """ + Queries the database for the list of all clientes of a fornecedor. + + Args: + db_file_path (str): The path to the database file. + fornecedor_identificador (str): The fornecedor's identificador. + + Returns: + list: The list of clientes of the fornecedor. + """ + with closing(sqlite3.connect(db_file_path)) as connection: + with closing(connection.cursor()) as cursor: + return [Cliente(i, n, e) for i, n, e in + cursor.execute(""" + SELECT DISTINCT clientes.identificador, clientes.nome, clientes.endereco + FROM clientes + INNER JOIN notas_fiscais ON clientes._id = notas_fiscais.cliente_id + INNER JOIN fornecedores ON notas_fiscais.fornecedor_id = fornecedores._id + WHERE fornecedores.identificador = ?; + """, (fornecedor_identificador, )).fetchall() + ] diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..5b654e6 --- /dev/null +++ b/src/models.py @@ -0,0 +1,40 @@ +from datetime import datetime + +class Fornecedor(): + """ + Class that represents a fornecedor. + + Attributes: + identificador (str): The fornecedor's identifier. + """ + def __init__(self, identificador): + self.identificador = identificador + +class Cliente(): + """ + Class that represents a cliente. + + Attributes: + identificador (str): The cliente's identifier. + nome (str): The cliente's name. + endereco (str): The cliente's address. + """ + def __init__(self, identificador, nome, endereco): + self.identificador = identificador + self.nome = nome + self.endereco = endereco + +class NotaFiscal(): + """ + Class that represents a nota fiscal. + + Attributes: + fornecedor (Fornecedor): The fornecedor of the nota fiscal. + cliente (Cliente): The cliente of the nota fiscal. + boletos (list): The list of boletos of the nota fiscal. + """ + def __init__(self, chave_acesso, fornecedor, cliente, boletos): + self.chave_acesso = chave_acesso + self.fornecedor = fornecedor + self.cliente = cliente + self.boletos = boletos diff --git a/src/parsing.py b/src/parsing.py new file mode 100644 index 0000000..2736e34 --- /dev/null +++ b/src/parsing.py @@ -0,0 +1,83 @@ +import xml.etree.ElementTree as ET +import re +from models import Fornecedor, Cliente, NotaFiscal +from datetime import datetime + +def parse_fornecedor(fornecedor_xml, namespace=''): + """ + Parses a fornecedor from an XML element. + + Args: + fornecedor_xml (xml.etree.ElementTree.Element): The XML element to parse. + namespace (str): The namespace of the XML element. + + Returns: + Fornecedor: The parsed fornecedor. + """ + if (cnpj := fornecedor_xml.find(f'{namespace}CNPJ')) is not None: + identificador = cnpj.text + else: + identificador = fornecedor_xml.find(f'{namespace}CPF').text + + return Fornecedor(identificador) + +def parse_cliente(cliente_xml, namespace=''): + """" + Parses a cliente from an XML element. + + Args: + cliente_xml (xml.etree.ElementTree.Element): The XML element to parse. + namespace (str): The namespace of the XML element. + + Returns: + Cliente: The parsed cliente. + """ + if (cnpj := cliente_xml.find(f'{namespace}CNPJ')) is not None: + identificador = cnpj.text + else: + identificador = cliente_xml.find(f'{namespace}CPF').text + nome = cliente_xml.find(f'{namespace}xNome').text + + endereco_xml = cliente_xml.find(f'{namespace}enderDest') + logradouro = endereco_xml.find(f'{namespace}xLgr').text + numero = endereco_xml.find(f'{namespace}nro').text + municipio = endereco_xml.find(f'{namespace}xMun').text + unidade_federativa = endereco_xml.find(f'{namespace}UF').text + endereco = f'{logradouro}, Nº {numero}, {municipio} - {unidade_federativa}' + + return Cliente(identificador, nome, endereco) + +def parse_nota_fiscal(file_content): + """ + Parses a nota fiscal from an XML file. + + Args: + file_content (str): The XML file content. + + Returns: + NotaFiscal/None: The parsed nota fiscal or None if the file is not a nota fiscal. + """ + try: + root_xml = ET.fromstring(file_content) + nfe_xml = root_xml[0] # NFe + inf_nfe_xml = nfe_xml[0] # infNFe + namespace = regex.group(0) if (regex := re.match(r'\{.*\}', root_xml.tag)) else '' # xmlns + + fornecedor = parse_fornecedor(inf_nfe_xml.find(f'{namespace}emit'), namespace) + cliente = parse_cliente(inf_nfe_xml.find(f'{namespace}dest'), namespace) + + cobranca_xml = inf_nfe_xml.find(f'{namespace}cobr') + duplicata_xml = cobranca_xml.findall(f'{namespace}dup') + boletos = [ + { + 'valor': float(dup_xml.find(f'{namespace}vDup').text), + 'vencimento': datetime.strptime(dup_xml.find(f'{namespace}dVenc').text, '%Y-%m-%d') + } + for dup_xml in duplicata_xml + ] + + return NotaFiscal(inf_nfe_xml.attrib['Id'][3:], fornecedor, cliente, boletos) + + except Exception: + return None + diff --git a/src/service.py b/src/service.py new file mode 100644 index 0000000..71331b1 --- /dev/null +++ b/src/service.py @@ -0,0 +1,63 @@ +from flask import Flask, redirect, render_template, request, url_for +from werkzeug.utils import secure_filename +import re +from parsing import parse_nota_fiscal +from database import create_database as create_database_original, \ + create_nota_fiscal as create_nota_fiscal_original, \ + query1 as query1_original, \ + query2 as query2_original + +# Setup functions for local database +DB_FILE_PATH = 'db/database.db' +DB_SCHEMA_PATH = 'db/schema.sql' +create_database = lambda: create_database_original(DB_FILE_PATH, DB_SCHEMA_PATH) +create_nota_fiscal = lambda x: create_nota_fiscal_original(DB_FILE_PATH, x) +query1 = lambda x: query1_original(DB_FILE_PATH, x) +query2 = lambda x: query2_original(DB_FILE_PATH, x) + +# Setup Flask app +app = Flask(__name__) + +@app.route('/') +def page_1(): + return render_template('1.html') + +@app.route('/2', methods = ['GET', 'POST']) +def page_2(): + if request.method != 'POST': + # To avoid breaking the flow of application, we will redirect to the first page + return redirect(url_for('page_1')) + + # Get the list of files from the request + files = request.files.getlist("file") + for file in files: + if file.filename == '': + continue + nf = parse_nota_fiscal(file.read()) # Parse the file + if nf is not None: + create_nota_fiscal(nf) # Create the nota fiscal in database + + return render_template('2.html') + +@app.route('/3', methods = ['GET', 'POST']) +def page_3(): + if request.method != 'POST': + # To avoid breaking the flow of application, we will redirect to the first page + return redirect(url_for('page_1')) + # Get the identificador from the request without the following characters . - / + identificador = re.sub(r'[\.\-\/]', '', request.form.get('identificador', '')) + boletos = query1(identificador) # Get all boletos from a fornecedor + clientes = query2(identificador) # Get all clientes related to a fornecedor + + return render_template('3.html', clientes=clientes, boletos=boletos) + +@app.template_filter() +def format_datetime(value): + return value.strftime('%d/%m/%Y') + +if __name__ == '__main__': + try: + create_database() # Create the database + except Exception: + print("Não foi possível criar o banco de dados") + app.run("0.0.0.0", port=5000, debug = True) # Run the app diff --git a/src/static/styles.css b/src/static/styles.css new file mode 100644 index 0000000..ebe82ce --- /dev/null +++ b/src/static/styles.css @@ -0,0 +1,133 @@ +body { + background-image: linear-gradient(50deg, #000 0%, #e2e2e2 100%); + height: 100vh; + font-family: 'Montserrat', sans-serif; +} + +.container { + position: absolute; + transform: translate(-50%,-50%); + top: 50%; + left: 50%; +} + +form { + background-color: rgba(255,255,255,0.1); + padding: 3em; + min-height: 320px; + border-radius: 20px; + border-left: 1px solid rgba(255,255,255,0.1); + border-top: 1px solid rgba(255,255,255,0.1); + backdrop-filter: blur(10px); + box-shadow: 20px 20px 40px -6px rgba(0,0,0,0.2); + text-align: center; + position: relative; + transition: all 0.2s ease-in-out; +} + +p { + font-weight: 500; + color: #fff; + opacity: 0.7; + text-shadow: 2px 2px 4px rgba(0,0,0,0.2); +} + +.title { + font-size: 1.4rem; + margin-top: 0px; + margin-bottom: 50px; +} + +.sub-title { + font-size: 1.2rem; +} + +input, label { + background: transparent; + min-width: 200px; + padding: 1em; + margin-bottom: 2em; + border: none; + border-left: 1px solid rgba(255,255,255,0.1); + border-top: 1px solid rgba(255,255,255,0.1); + border-radius: 5000px; + box-shadow: 4px 4px 60px rgba(0,0,0,0.2); + color: #fff; + font-family: Montserrat, sans-serif; + font-weight: 500; + transition: all 0.2s ease-in-out; + text-shadow: 2px 2px 4px rgba(0,0,0,0.2); + text-align:center; +} + +input:hover, label:hover { + background: rgba(255,255,255,0.1); + box-shadow: 4px 4px 60px 8px rgba(0,0,0,0.2); +} + +input:focus, label:focus { + background: rgba(255,255,255,0.1); + box-shadow: 4px 4px 60px 8px rgba(0,0,0,0.2); +} + +input::placeholder { + font-family: Montserrat, sans-serif; + font-weight: 400; + color: #fff; + text-shadow: 2px 2px 4px rgba(0,0,0,0.4); +} + +input[type="submit"] { + margin-top: 10px; + font-size: 1rem; + position: absolute; + transform: translate(-50%,0%); + bottom: 0%; + left: 50%; +} + +input[type="submit"]:hover { + cursor: pointer; +} + +input[type="submit"]:active { + background: rgba(255,255,255,0.2); +} + +input:focus, +button:focus { + outline: none; +} + +input[type="file"] { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; +} + +label { + cursor: pointer; +} + +ul { + padding-inline-start: 0px; +} + +li { + list-style-type: none; + max-width: 340px; + padding: 1em; + margin-bottom: 1em; + border-radius: 5px; + box-shadow: 2px 2px 15px rgb(0 0 0 / 20%); + color: #fff; + font-family: Montserrat, sans-serif; + font-weight: 500; + text-shadow: 2px 2px 4px rgb(0 0 0 / 20%); + text-align: center; + display: flex; + flex-direction: column; +} diff --git a/src/templates/1.html b/src/templates/1.html new file mode 100644 index 0000000..f36864c --- /dev/null +++ b/src/templates/1.html @@ -0,0 +1,38 @@ + + + + + + + Venha para Recomb + + +
+
+

Upload do Arquivo

+ +
+

+

Caso nenhum arquivo seja selecionado,
os dados do banco serão utilizados

+
+
+
+ + + \ No newline at end of file diff --git a/src/templates/2.html b/src/templates/2.html new file mode 100644 index 0000000..bcb1bba --- /dev/null +++ b/src/templates/2.html @@ -0,0 +1,18 @@ + + + + + + + Venha para Recomb + + +
+
+

Informações do Fornecedor

+
+
+
+
+ + \ No newline at end of file diff --git a/src/templates/3.html b/src/templates/3.html new file mode 100644 index 0000000..1288946 --- /dev/null +++ b/src/templates/3.html @@ -0,0 +1,48 @@ + + + + + + + Venha para Recomb + + +
+
+

Resultados

+

Clientes

+ {% if clientes|length > 0 %} + + {% else %} +

Nenhum cliente foi encontrado

+ {% endif %} + +
+ +

Boletos

+ {% if boletos|length > 0 %} + + {% else %} +

Nenhum boleto foi encontrado

+ {% endif %} +
+
+
+
+ + \ No newline at end of file