fix: correção no modelo tecnospeed

This commit is contained in:
Baltazar Barbosa 2026-07-10 16:28:18 -03:00
commit 343b30a19f
31 changed files with 2909 additions and 0 deletions

216
.gitignore vendored Normal file
View file

@ -0,0 +1,216 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
system_config.json
inserts_teste.sql
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Diretorio de classes acessorias
Model/
utils/
logs/
src/models/sg_tray_models/
src/models/servico_titulos_receber/

34
Dockerfile Normal file
View file

@ -0,0 +1,34 @@
FROM python:3.12-slim AS builder
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8
RUN apt-get update && apt-get install -y --no-install-recommends \
locales vim iputils-ping build-essential && \
locale-gen en_US.UTF-8 && \
apt-get clean && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir unidecode
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8
RUN apt-get update && apt-get install -y --no-install-recommends \
locales vim iputils-ping && \
locale-gen en_US.UTF-8 && \
apt-get clean && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
RUN mkdir /app/logs
RUN mkdir /app/controller
COPY src .
COPY utils/Exchange2.py utils/exchangeModels.py utils/HostMetrics.py /app
# COPY Model/sg_tray_models/services_model.py /app/models/sg_tray_models/
# COPY Model/servico_titulos_receber/contract_Tittles.py /app/models/servico_titulos_receber/
# COPY Model/servico_titulos_receber/tittle_model.py /app/models/servico_titulos_receber/
CMD ["python", "controller.py"]

75
README.md Normal file
View file

@ -0,0 +1,75 @@
# Innovar AMR Tray Controller
Microserviço responsável por atuar como controlador de integrações entre a plataforma de e-commerce Tray, o emissor de NFe Plugnotas e o ERP Sienge.
## Visão Geral
Este projeto consome mensagens de uma fila RabbitMQ (originadas de webhooks) e orquestra a comunicação entre diferentes sistemas para automatizar o fluxo de vendas:
1. **Tray (E-commerce):**
- Recebe webhooks de criação (`insert`) e atualização (`update`) de pedidos.
- Busca dados completos do pedido na API da Tray.
- Atualiza dados fiscais (NFe) no pedido dentro da Tray após faturamento.
2. **Plugnotas (Notas Fiscais):**
- Envia dados do pedido para emissão de Nota Fiscal Eletrônica.
- Recebe webhooks informando a conclusão da emissão da NFe, extraindo a chave de acesso, o número e os produtos a partir do XML retornado (base64).
3. **Sienge (ERP - Integração via Filas/RPC):**
- **Clientes:** Envia requisições (RPC) para criação de clientes no Sienge quando um novo pedido é gerado.
- **Títulos a Receber:** Envia requisições (RPC) para geração de títulos financeiros no Sienge após a emissão da NFe.
## Fluxo de Processamento
### 1. Webhook Tray (`Recebimento`)
- **Ação `insert`:** O controlador identifica o novo pedido, cria/sincroniza o cliente no ERP Sienge e atualiza o ID do cliente no banco de dados local.
- **Ação `update`:** Caso o pedido passe para o status "A ENVIAR", o serviço envia os dados mapeados para a fila de emissão de NFe, sinalizando no banco local que o envio ocorreu.
### 2. Webhook Plugnotas (`RecebimentoPlugnotas`)
- Ao receber a confirmação de que a NFe foi gerada com status "CONCLUIDO":
- Decodifica e extrai as informações pertinentes da nota a partir do XML da NFe.
- Envia a ordem para criar o título a receber no ERP (Sienge).
- Atualiza a nota fiscal (chave, link, xml) diretamente no painel da Tray.
- Grava o status atualizado no banco de dados local.
## Tecnologias e Bibliotecas
- **Python 3**
- **Bibliotecas Principais:** Pydantic (validação de dados), SQLAlchemy/Psycopg2 (banco de dados), Redis, Requests, Pika (RabbitMQ).
- **Mensageria:** RabbitMQ para filas assíncronas e RPC, facilitado pelas bibliotecas base da equipe (`Exchange2`).
- **Banco de Dados:** PostgreSQL (Persistência de estado dos pedidos, status de integrações e logs).
- **Cache / Configuração:** Redis (Armazenamento e busca das credenciais e configurações de ambiente em tempo real).
- **Infraestrutura:** Docker & Docker Compose.
## Como Executar
### Pré-requisitos
- Docker e Docker Compose instalados no ambiente de execução.
- Acesso aos repositórios Git base (`Exchange2` e `SoftGOModel`) pois o script de build fará o clone de dependências proprietárias.
- Instâncias do Redis, RabbitMQ e PostgreSQL acessíveis na rede (`sg_base_network`).
### Passos para Build e Deploy
1. Clone o repositório e navegue até a pasta raiz.
2. Execute o script de build para baixar as dependências e criar a imagem Docker:
```bash
chmod +x buildImages.sh
./buildImages.sh
```
3. Inicie o container com o Docker Compose:
```bash
docker-compose up -d
```
## Variáveis de Ambiente e Configurações
O container do `controller` possui poucas configurações no `docker-compose.yml`, pois o sistema busca suas credenciais principais (banco, AMQP, endpoints, chaves de API da Tray, etc.) diretamente de uma chave JSON no **Redis** (`innovar_amr_tray-system_config-innovar`).
Certifique-se de que essa chave JSON esteja corretamente populada no Redis para que o microserviço consiga inicializar.
## Estrutura de Diretórios
- `src/controller.py`: Ponto de entrada do serviço e consumidor principal da fila RabbitMQ.
- `src/services/`: Regras de negócio, transformações e mapeamentos (Tray para Sienge, Tray para NFe).
- `src/models/`: Definições Pydantic para validação das estruturas de dados.
- `src/api/`: Comunicação HTTP direta com APIs externas.
- `src/db/`: Instância e métodos de comunicação com o PostgreSQL local.

96
buildImages.sh Normal file
View file

@ -0,0 +1,96 @@
#!/bin/bash
version=':v1' # VERSIONAMENTO MACRO - TODOS SERVIÇOS NA MESMA VERSÃO SÃO COMPATÍVEIS ENTRE SI
nome_diretorio="innovar_amr_tray" #$(basename "$PWD")
system="${nome_diretorio,,}"
SYSTEM="${system^^}"
# Try to find the correct $DOCKER_COMPOSE_CMD command
if command -v docker-compose &> /dev/null; then
DOCKER_COMPOSE_CMD="docker-compose"
elif command -v docker &> /dev/null && docker compose version &> /dev/null; then
DOCKER_COMPOSE_CMD="docker compose"
else
echo "$DOCKER_COMPOSE_CMD is not installed."
exit 1
fi
clone() {
echo "VERSAO ATUAL CLASSES ACESSORIAS Exchange2 SoftGOModel"
$DOCKER_COMPOSE_CMD down
if [ -d "utils" ]; then
rm -rf utils
fi
if [ -d "Model" ]; then
rm -rf Model
fi
# PROJETOS DE BASE
git clone git@github.com:desenvolvimentopsa/Exchange2.git utils/Exchange2
git clone git@github.com:desenvolvimentopsa/SoftGOModel.git Model
cd utils
find . -name "*.py" | xargs -I {} cp {} .
rm -rf Exchange2 SOFTGO_AUTH_MIDDLEWARE
cd ..
# remove .git e .gitignore do Model
cd Model
find . \( -name ".git" -o -name ".gitignore" \) -exec rm -rf {} +
cd ..
}
for arg in "$@"; do
if [ -n "$arg" ] && [ "$arg" == "git" ]; then
echo "VERSAO ATUAL DO GIT ${version}"
git stash
git pull
fi
done
if [ ! -d "utils" ] || [ ! -d "Model" ]; then
echo "DOWNLOAD CLASSES ACESSORIAS"
clone
else
for arg in "$@"; do
if [ -n "$arg" ] && [ "$arg" == "clone" ]; then
echo "DOWNLOAD CLASSES ACESSORIAS"
clone
fi
done
fi
for arg in "$@"; do
if [ -n "$arg" ] && [ "$arg" == "prune" ]; then
echo "REMOVER IMAGENS ${system} ${version}"
docker image ls -q -f "reference=${system}-*${version}" | xargs -I {} docker image rm -f {}
docker system prune -af
fi
done
# EDITE DE ACORDO COM OS SERVIÇOS QUE VAI IMPLEMENTAR
# NOME DO DIRETORIO É O NOME DO SISTEMA E CADA DIRETORIO UM SERVICO
# CASO UM UNICO SERVIÇO MANTENHA O MODELO PARA PODERMOS IMPLEMENTAR UM DEPLOY AUTOMATICO NO FUTURO
#==> MODELO
echo "GERAR IMAGENS ${system} ${version}"
service="controller"
echo $service
echo "COPIAR ARQUIVOS PARA BUILD ${system} ${version}"
cp -r Model/sg_tray_models ./src/models/sg_tray_models/
cp -r Model/servico_titulos_receber ./src/models/servico_titulos_receber/
docker build -t ${system}-${service}${version} .
rm -rf ./src/models/sg_tray_models/*
rm -rf ./src/models/servico_titulos_receber/*
cd ./src
ln -sf ../utils/Exchange2.py .
ln -sf ../utils/exchangeModels.py .
ln -sf ../utils/HostMetrics.py .
ln -sf ../Model/sg_tray_models/services_model.py ./models/sg_tray_models/services_model.py
ln -sf ../Model/servico_titulos_receber/contract_Tittles.py ./models/servico_titulos_receber/contract_Tittles.py
ln -sf ../Model/servico_titulos_receber/tittle_model.py ./models/servico_titulos_receber/tittle_model.py
cd ..

20
docker-compose.yml Normal file
View file

@ -0,0 +1,20 @@
networks:
local:
sg_base_network:
name: sg_base_network
external: true
services:
controller:
image: innovar_amr_tray-controller:v1
#restart: always
volumes:
- ./logs/controller:/app/logs
- /etc/hostname:/app/hostname
- /proc:/app/host-proc
- /metrics:/app/metrics
environment:
- "TZ=America/Sao_Paulo"
networks:
- local
- sg_base_network

28
envio_cliente.json Normal file
View file

@ -0,0 +1,28 @@
{
"config": {
"usuario": "api_user",
"senha": "api_password",
"subdominio": "empresa_exemplo"
},
"cliente": {
"naturalPersonData": {
"name": "João da Silva",
"cpf": "12345678901",
"email": "joao@exemplo.com",
"birthDate": "1990-01-01",
"civilStatus": "Casado",
"profession": "Engenheiro"
},
"addresses": [
{
"type": "RES",
"streetName": "Rua Exemplo",
"number": "123",
"neighborhood": "Centro",
"city": "São Paulo",
"state": "SP",
"zipCode": "01001000"
}
]
}
}

View file

@ -0,0 +1,88 @@
{
"fila": "nfe",
"metodo": "post",
"cliente": "{{cliente}}",
"dados": {
"idIntegracao": "NFe_TESTE_001",
"presencial": true,
"consumidorFinal": true,
"natureza": "OPERAÇÃO INTERNA",
"emitente": {
"cpfCnpj": "00000000000000",
"inscricaoEstadual": "ISENTO"
},
"destinatario": {
"cpfCnpj": "08114280956",
"razaoSocial": "NF-E EMITIDA EM HOMOLOGACAO",
"email": "contato@tecnospeed.com.br",
"endereco": {
"logradouro": "AVENIDA DUQUE DE CAXIAS",
"numero": "882",
"bairro": "CENTRO",
"codigoCidade": "4115200",
"descricaoCidade": "MARINGA",
"estado": "PR",
"cep": "87020025"
}
},
"itens": [
{
"codigo": "1",
"descricao": "PRODUTO DE TESTE",
"ncm": "06029090",
"cest": "0123456",
"cfop": "5101",
"valorUnitario": {
"comercial": 4.67,
"tributavel": 4.67
},
"valor": 4.67,
"tributos": {
"icms": {
"origem": "0",
"cst": "00",
"baseCalculo": {
"modalidadeDeterminacao": 0,
"valor": 0
},
"aliquota": 0,
"valor": 0
},
"pis": {
"cst": "99",
"baseCalculo": {
"valor": 0,
"quantidade": 0
},
"aliquota": 0,
"valor": 0
},
"cofins": {
"cst": "07",
"baseCalculo": {
"valor": 0
},
"aliquota": 0,
"valor": 0
}
}
}
],
"pagamentos": [
{
"aVista": true,
"meio": "01",
"valor": 4.67
}
],
"responsavelTecnico": {
"cpfCnpj": "08187168000160",
"nome": "Tecnospeed",
"email": "contato@tecnospeed.com.br",
"telefone": {
"ddd": "44",
"numero": "30379500"
}
}
}
}

File diff suppressed because one or more lines are too long

16
payload_nfe_tray.json Normal file

File diff suppressed because one or more lines are too long

262
pedido_completo.json Normal file
View file

@ -0,0 +1,262 @@
{
"Order": {
"status": "A ENVIAR VINDI",
"id": "12797",
"date": "2025-10-01",
"hour": "09:07:57",
"customer_id": "11531",
"partial_total": "69.90",
"taxes": "0.00",
"discount": "0.00",
"point_sale": "PARTICULAR",
"shipment": "Retirada",
"shipment_value": "0.00",
"original_shipment_value": "0.00",
"shipment_date": "",
"delivered": "",
"delivered_status": "",
"shipping_cancelled": "0",
"store_note": "",
"customer_note": "",
"partner_id": "",
"discount_coupon": "",
"client_ip": "",
"payment_method_rate": "0.00",
"installment": "0",
"value_1": "0.00",
"sending_code": "",
"sending_date": "0000-00-00",
"billing_address": "",
"delivery_time": "",
"payment_method_id": "",
"payment_method": "PIX",
"session_id": "0BBB15A404B6BA1",
"total": "69.90",
"payment_date": "2025-10-01",
"access_code": "834E9A51CFF9D99",
"shipment_integrator": "",
"modified": "2025-10-30 10:22:38",
"printed": "",
"interest": "0.00",
"cart_additional_values_discount": "0.00",
"cart_additional_values_increase": "0.00",
"id_quotation": "",
"estimated_delivery_date": "2025-10-01",
"is_traceable": "0",
"external_code": "",
"tracking_url": "",
"has_payment": "1",
"has_shipment": "0",
"has_invoice": "0",
"delivery_date": "",
"dc_id": "",
"dc_order_origin": "",
"total_comission_user": "0.00",
"total_comission": "0.00",
"OrderStatus": {
"id": "995",
"default": "0",
"type": "open",
"show_backoffice": "1",
"allow_edit_order": "1",
"description": "",
"status": "A ENVIAR VINDI",
"show_status_central": "1",
"background": "#60e163",
"display_name": "A enviar Vindi",
"font_color": "#000000"
},
"PickupLocation": [],
"cost": "0.00",
"app_id": "0",
"urls": {
"payment": "https://1225878.commercesuite.com.br/loja/pagamento.php?loja=1225878&pedido=834E9A51CFF9D99"
},
"store_segment": "Animais & petshop",
"payment_method_type": "",
"Customer": {
"cnpj": "",
"newsletter": "0",
"created": "2025-10-01 08:05:12",
"terms": "0000-00-00 00:00:00",
"id": "11531",
"name": "Henrique Crespilho Ferro",
"registration_date": "2025-10-01",
"rg": "",
"cpf": "04650716004",
"phone": "14991239292",
"cellphone": "14991239292",
"birth_date": "",
"gender": "0",
"email": "gazelle30064@aminating.com",
"nickname": "",
"token": "834E9A51CFF9D99",
"total_orders": "0",
"observation": "",
"type": "0",
"foreign": "0",
"company_name": "",
"state_inscription": "",
"reseller": "",
"discount": "0.00",
"blocked": "0",
"credit_limit": "0.00",
"indicator_id": "0",
"profile_customer_id": "1",
"last_sending_newsletter": "0000-00-00",
"last_purchase": "2025-10-01",
"last_visit": "2025-10-01",
"last_modification": "0000-00-00 00:00:00",
"address": "Av. Dr. Quinzinho",
"zip_code": "17210-110",
"number": "511",
"complement": "Estacionamento Jaú Shopping",
"neighborhood": "Vila Assis",
"city": "Jaú",
"state": "SP",
"country": "Brasil",
"modified": "2025-10-01 09:07:57",
"count_orders": "0",
"Extensions": {
"Profile": {
"id": "1",
"name": "Padrao",
"approves_registration": "0"
},
"Profiles": [
{
"id": "1",
"price_list_id": "0",
"name": "Padrao",
"approves_registration": "0",
"show_price": "0",
"theme_id": "0",
"selected": "1"
}
]
},
"CustomerAddresses": [
{
"CustomerAddress": {
"id": "9285",
"customer_id": "11531",
"address": "Av. Dr. Quinzinho",
"number": "511",
"complement": "Estacionamento Jaú Shopping",
"neighborhood": "Vila Assis",
"city": "Jaú",
"state": "SP",
"zip_code": "17210-110",
"country": "Brasil",
"type": "1",
"active": "1",
"description": "",
"recipient": "",
"type_delivery": "1",
"not_list": "0"
}
}
]
},
"ProductsSold": [
{
"ProductsSold": {
"product_kit_id": "0",
"product_kit_id_kit": "0",
"id_campaign": "",
"product_id": "63",
"quantity": "1",
"id": "16113",
"order_id": "12797",
"name": "MMM Calça de Malha Foil Preta Brilhante<br /><strong>Cor</strong> PRETO<br /><strong>TAMANHO</strong> 12 (Disponibilidade: Imediata)<br />",
"original_name": "",
"virtual_product": "0",
"ean": "",
"availability_days": "",
"availability": "",
"Sku": [],
"price": "69.90",
"cost_price": "0.00",
"original_price": "69.90",
"weight": "136",
"weight_cubic": "110",
"brand": "kamylus INV",
"model": "",
"reference": "",
"length": "24",
"width": "22",
"height": "1",
"variant_id": "1132951",
"additional_information": "",
"text_variant": "",
"warranty": "",
"bought_together_id": "0",
"ncm": "",
"included_items": "",
"release_date": "",
"commissioner_value": "",
"comissao": "0.00",
"is_giveaway_by_coupon": "0",
"ProductSoldImage": [],
"Category": [],
"is_giveaway": "0",
"BoughtTogether": [],
"ProductSoldPackage": [],
"ProductSoldCard": [],
"url": {
"http": "",
"https": ""
},
"Discount": [],
"Stock": {
"id": "1",
"name": "Loja"
}
}
}
],
"OrderInvoice": [],
"Payment": [
{
"Payment": {
"created": "2025-10-01 09:24:13",
"modified": "2025-10-01 09:24:12",
"id": "2691",
"order_id": "12797",
"payment_method_id": "",
"method": "PIX",
"payment_place": "",
"value": "66.41",
"date": "2025-10-01",
"note": "Pagamento realizado com sucesso utilizando ASSAS com o metodo de pagamento PIX",
"unique_number": ""
}
}
],
"MlOrder": [],
"MarketplaceOrder": [],
"OrderTransactions": [],
"OrderInvoiceAmount": {
"reference": "2025-10-01",
"value": "0.70",
"tax_type": "%",
"tax_value": "1.00",
"point_sale": "PARTICULAR"
},
"OtherInvoiceAmounts": [],
"ExtraTabs": [],
"OrderChilds": [],
"interest_paid_by": "seller",
"DistributionCenter": {
"status": "pending",
"type": "main_order",
"Package": ""
},
"PaymentMethodMessage": [],
"partner_name": "",
"Transaction": ""
},
"Extensions": [],
"User": [],
"Confirmation": []
}

21
requirements.txt Normal file
View file

@ -0,0 +1,21 @@
annotated-types==0.7.0
certifi==2026.2.25
cffi==2.0.0
charset-normalizer==3.4.7
cryptography==46.0.7
cryptograpy==0.0.0
greenlet==3.5.0
idna==3.11
pika==1.3.2
psutil==7.2.2
psycopg2-binary==2.9.12
pycparser==3.0
pydantic==2.13.2
pydantic_core==2.46.2
pytz==2026.1.post1
redis==7.4.0
requests==2.33.1
SQLAlchemy==2.0.49
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.6.3

1
src/Exchange2.py Symbolic link
View file

@ -0,0 +1 @@
../utils/Exchange2.py

1
src/HostMetrics.py Symbolic link
View file

@ -0,0 +1 @@
../utils/HostMetrics.py

15
src/api/publicas.py Normal file
View file

@ -0,0 +1,15 @@
import requests
import time
def busca_cep(cep: str) -> dict:
while True:
try:
url = f"https://viacep.com.br/ws/{cep}/json/"
response = requests.get(url)
if response.status_code == 200:
return response.json()
else:
time.sleep(0.1)
except Exception as e:
time.sleep(0.1)

104
src/api/tray.py Normal file
View file

@ -0,0 +1,104 @@
from models.sg_tray_models.services_model import TrayOrderRootResponse
from appIC2Builder import SystemConfig
import requests
import redis
import json
class ApiTray:
def __init__(self, system_config: SystemConfig, redis_client: redis.Redis):
self.consumer_key = system_config.tray_consumer_key
self.consumer_secret = system_config.tray_consumer_secret
self.code = system_config.tray_code
self.url = system_config.tray_url
self.redis_client = redis_client
self.service = system_config.service
self.system = system_config.system
def auth(self) -> str:
try:
print("\n")
print("Autenticando na API da Tray...", flush=True)
response = requests.post(url = f"{self.url}/web_api/auth",
headers = {"Content-Type": "application/x-www-form-urlencoded"},
data = f"consumer_key={self.consumer_key}&consumer_secret={self.consumer_secret}&code={self.code}")
if response.status_code in [ 200, 201 ]:
print("Autenticado com sucesso!", flush=True)
return response.json()
else:
print(f"Erro ao autenticar na Tray: {response.status_code} | {response.text}", flush=True)
raise Exception(response.text)
except Exception as e:
print(f"Error connecting to Tray: {e}", flush=True)
raise e
def refresh_token(self) -> str:
try:
print("\n")
print("Atualizando token de acesso da Tray...", flush=True)
response = requests.get(url = f"{self.url}/web_api/auth?access_token={self.get_token()}")
if response.status_code in [ 200, 201 ]:
print("Token atualizado com sucesso!", flush=True)
return response.json()
else:
print(f"Erro ao atualizar token na Tray: {response.status_code} | {response.text}", flush=True)
raise Exception(response.text)
except Exception as e:
print(f"Error connecting to Tray: {e}", flush=True)
raise e
def get_token(self) -> str:
try:
chave = f"{self.system}_{self.service}_auth"
if self.redis_client.exists(chave):
auth = json.loads(self.redis_client.get(chave))
return auth['access_token']
# Busca o token e guarda no redis com cache de 2h e 55 minutos
token = self.auth()
self.redis_client.set(chave, json.dumps(token), ex=(2*3600))
return token['access_token']
except Exception as e:
raise e
def get_pedido_completo(self, id_pedido: int) -> TrayOrderRootResponse:
try:
response = requests.get(url = f"{self.url}/web_api/orders/{id_pedido}/complete?access_token={self.get_token()}")
if response.status_code == 200:
print(f"Pedido {id_pedido} obtido com sucesso!", flush=True)
return TrayOrderRootResponse(**response.json())
else:
print(f"Erro ao obter pedido {id_pedido} na Tray: {response.text}", flush=True)
raise Exception(response.text)
except Exception as e:
print(f"Error connecting to Tray: {e}", flush=True)
raise e
def inserir_nfe_pedido(self, id_pedido: int, nfe: dict) -> None:
try:
response = requests.post(url = f"{self.url}/web_api/orders/{id_pedido}/invoices?access_token={self.get_token()}",
headers = {"Content-Type": "application/json"},
data = json.dumps(nfe))
if response.status_code == 200:
print(f"NFE do pedido {id_pedido} atualizada com sucesso!", flush=True)
else:
print(f"Erro ao atualizar NFE do pedido {id_pedido} na Tray: {response.text}", flush=True)
raise Exception(response.text)
except Exception as e:
print(f"Error connecting to Tray: {e}", flush=True)
raise e
def inserir_produto_tray(self, produto: dict) -> int:
try:
response = requests.post(url = f"{self.url}/web_api/products?access_token={self.get_token()}",
headers = {"Content-Type": "application/json"},
data = json.dumps(produto))
if response.status_code in [ 200, 201 ]:
print(f"Produto {produto['name']} inserido com sucesso!", flush=True)
return response.json()['id']
else:
print(f"Erro ao inserir produto {produto['name']} na Tray: {response.text}", flush=True)
raise Exception(response.text)
except Exception as e:
print(f"Error connecting to Tray: {e}", flush=True)
raise e

124
src/appIC2Builder.py Normal file
View file

@ -0,0 +1,124 @@
from datetime import datetime as dt
from typing import Tuple, Any
from exchangeModels import *
from Exchange2 import IC2Builder
class SystemConfig(BaseModel):
amqps : str
ic2_url : str
rabbitmq : str
system : str
service : str
client : str
enterprise_id : str
enterprise_name : str
servicekey : str
serviceid : str
tray_consumer_key : str
tray_consumer_secret: str
tray_code : str
tray_url : str
db_host : str
db_port : int
db_user : str
db_password : str
db_name : str
# AQUI PODEMOS ESTENDER OS REALMS TYPE DE ACORDO COM A APLICAÇÃO
# Dados cria o objeto que será passado para o IC2Builder na inicialização
class Dados(BaseModel):
service_date : str
startup_date : str
task_id : str
service : str
versionstr : str
origin : str
enterprise_id : str
enterprise_name : str
system : str
source_key : int | None = 0
destination_key : int | None = 0
# ESPECIALIZAÇÃO REALM OPERATIONAL - AQUI EXEMPLO PARA MIDDLEWARE DE VENDAS
class Operation(ICOperational):
source_key : int = 0
destination_key : int = 0
class AppIC2Builder(IC2Builder):
def __init__(self, dados: object): # AQUI dados TEM INFORMACOES EM TEMPO DE INICIALIZAÇÃO
self.dados = dados
def _setLogs(self, message: tuple) -> InfoConnector: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO
msg, *extra = message
return InfoConnector(
realm = RealmType.LOGS,
messageIC = ICLogs(
log_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"),
service_date = self.dados.service_date,
service = self.dados.service,
versionstr = self.dados.versionstr,
origin = self.dados.origin,
enterprise_id = self.dados.enterprise_id,
enterprise_name = self.dados.enterprise_name,
message = msg)
)
def _setErrors(self, message: Tuple[str]) -> object: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO
return InfoConnector(
realm = RealmType.ERRORS,
messageIC = ICErrors(
log_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"),
service_date = self.dados.service_date,
service = self.dados.service,
versionstr = self.dados.versionstr,
origin = self.dados.origin,
enterprise_id = self.dados.enterprise_id,
enterprise_name = self.dados.enterprise_name,
message = message[0])
)
def _setOperational(self, message:Tuple[str,str,OpStatus]) -> object: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO
return InfoConnector(
realm = RealmType.OPERATIONAL,
messageIC = Operation(
log_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"),
service_date = self.dados.service_date, # DATA DE INICIO DO TASK/SERVIÇO
service_name = self.dados.service, # NOME DO SERVIÇO
task_id = self.dados.task_id, # ID DO TASK
startup_date = self.dados.startup_date, # DATA DE INICIO DO TASK/SERVIÇO
versionstr = self.dados.versionstr,
origin = self.dados.origin,
enterprise_id = self.dados.enterprise_id,
enterprise_name = self.dados.enterprise_name,
source_key = self.dados.source_key,
destination_key = self.dados.destination_key,
step_status = message[0], # step_status
message = message[1], # msg
status = message[2] # status
)
)
def _setStatus(self, message=Tuple[(int, float, float, float, float)]) -> object: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO
return InfoConnector(
realm = RealmType.STATUS,
messageIC = ICStatus(
service = self.dados.service,
versionstr = self.dados.versionstr,
system_name = self.dados.enterprise_name,
startup_date = self.dados.startup_date,
access_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"),
origin = self.dados.origin,
sid = self.dados.enterprise_id,
task_count = message[0],
cpu = message[1],
memory = message[2],
disk = message[3],
network = message[4])
)
def _setMetrics(self, message=Tuple[()]) -> object: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO
pass

655
src/controller.py Normal file
View file

@ -0,0 +1,655 @@
from datetime import datetime as dt
# pyrefly: ignore [missing-import]
from pydantic import ValidationError, TypeAdapter
from json.decoder import JSONDecodeError
from Exchange2 import QueueExchange2, LoggerExchange2
from exchangeModels import *
from HostMetrics import *
from appIC2Builder import *
from services.cliente import ClienteService
from services.nfe import NfeService
from services.titulo_receber import TituloReceberService
from db.banco import Banco
from models.recebimento import Recebimento, RecebimentoPlugnotas
from models.sg_tray_models.services_model import TrayOrderRootResponse
from api.tray import ApiTray
from send_rpc import SendRPCService
from send_queue import SendQueueService
import xml.etree.ElementTree as ET
import base64
import redis
import json
import time
import sys
# Versão do script e histórico básico
version = "1"
versionstr = f"v{version}.0.0 - BALT - 24/04/2026 - VERSÃO INICIAL"
system = "innovar_amr_tray"
service = "controller"
tenant = "innovar"
startup_date = dt.now().strftime("%Y-%m-%d %H:%M:%S")
TaskCount = 0
metrics = HostMetrics()
stats = metrics.get_metrics()
# Obtém o nome do ambiente/container
try:
with open("/app/hostname") as f:
origin= f.read().strip()
except Exception as e:
print(f"Montar o arquivo hostname no compose: {e}", flush=True)
time.sleep(5)
sys.exit(0)
############################################################## < Inicio logica > ##############################################################
def buscar_dados_tray(id_pedido_tray) -> TrayOrderRootResponse | None:
step_status = "BUSCANDO PEDIDO NO TRAY"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE))
try:
api_tray = ApiTray(system_config=system_config, redis_client=rd)
except Exception as e:
message = f"Erro ao inicializar ApiTray: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return None
try:
pedido = api_tray.get_pedido_completo(id_pedido_tray)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, f"Pedido encontrado: {pedido}", OpStatus.OK))
return pedido
except Exception as e:
message = f"Erro ao buscar pedido no Tray: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return None
def criar_cliente(pedido: TrayOrderRootResponse, banco: Banco) -> int | None:
step_status = "CRIANDO CLIENTE NO SIENGE"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE))
try:
cliente_service = ClienteService(config=system_config, redis_client=rd)
except Exception as e:
message = f"Erro ao inicializar ClienteService: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
logger.logMsgError(f"Erro ao inicializar ClienteService: {e}")
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CONFIGURAÇÃO DO CLIENTE",OpStatus.ACTIVE))
try:
config_tenant = banco.get_config_cliente(tenant=tenant)
if not config_tenant:
message = f"Configuração do cliente '{tenant}' não encontrada no banco de dados."
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
except Exception as e:
message = f"Erro ao obter configuração do cliente: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"MAPPING DOS DADOS",OpStatus.ACTIVE))
try:
payload = cliente_service._mapper_tray_to_sienge_cliente(pedido, config_tenant)
except Exception as e:
print(f"Erro ao mapear cliente: {e}",flush=True)
message = f"Erro ao mapear dados do cliente: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CREDENCIAIS DO INTEGRADOR",OpStatus.ACTIVE))
try:
integrador = banco.get_credenciais_servicos_integradores(integrador="cliente")
if not integrador:
message = f"Credenciais para integrador 'cliente' não encontradas no banco de dados."
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
except Exception as e:
message = f"Erro ao obter credenciais do integrador: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"ENVIANDO DADOS PARA FILA DE CLIENTE (RPC)",OpStatus.ACTIVE))
try:
rpc = SendRPCService(
system = integrador.system,
service = integrador.service,
amqps = integrador.amqps,
origin = origin,
serviceid = integrador.serviceid,
servicekey = integrador.servicekey
)
print(payload, flush=True)
response = rpc.send_rpc(payload=payload)
payload = response.get('payload')
hmac = response.get('hmac')
if not payload or not hmac:
message = f"Resposta RPC inválida: {response}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
response_data = rpc.get_payload(hmac, payload)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, f"Cliente criado: {response_data.get('customer_id')}", OpStatus.OK))
return response_data.get('customer_id')
except Exception as e:
message = f"Erro ao enviar dados para fila de cliente: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
def criar_titulo(pedido: TrayOrderRootResponse, banco: Banco, chave_nfe: str, numero_nfe: str) -> int | None:
step_status = "CRIANDO TÍTULO A RECEBER NO SIENGE"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE))
try:
titulo_service = TituloReceberService(config=system_config)
except Exception as e:
message = f"Erro ao inicializar TituloReceberService: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CONFIGURAÇÃO DO CLIENTE",OpStatus.ACTIVE))
try:
config_tenant = banco.get_config_cliente(tenant=tenant)
if not config_tenant:
message = f"Configuração do cliente '{tenant}' não encontrada no banco de dados."
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
except Exception as e:
message = f"Erro ao obter configuração do cliente: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO APROPRIAÇÃO FINANCEIRA",OpStatus.ACTIVE))
try:
apropriacao = banco.get_lista_apropriacao_financeira(tenant=tenant)
if not apropriacao:
message = f"Apropriação financeira para tenant '{tenant}' não encontrada no banco de dados."
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
except Exception as e:
message = f"Erro ao obter apropriação financeira: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO PEDIDO",OpStatus.ACTIVE))
try:
pedido_db = banco.get_pedido(id_pedido_tray=pedido.Order.id)
if not pedido_db:
message = f"Pedido '{pedido.id}' não encontrado no banco de dados."
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
except Exception as e:
message = f"Erro ao obter pedido: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"MAPPING DOS DADOS",OpStatus.ACTIVE))
try:
payload = titulo_service.map_tray_order_to_bill_data(pedido, config_tenant, apropriacao, pedido_db, chave_nfe, numero_nfe)
except Exception as e:
message = f"Erro ao mapear dados: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CREDENCIAIS DO INTEGRADOR",OpStatus.ACTIVE))
try:
integrador = banco.get_credenciais_servicos_integradores(integrador="titulo-cr")
if not integrador:
message = f"Credenciais para integrador 'titulo-cr' não encontradas no banco de dados."
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
except Exception as e:
message = f"Erro ao obter credenciais do integrador: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"ENVIANDO DADOS PARA FILA DE TÍTULO A RECEBER (RPC)",OpStatus.ACTIVE))
try:
rpc = SendRPCService(
system = integrador.system,
service = integrador.service,
amqps = integrador.amqps,
origin = origin,
serviceid = integrador.serviceid,
servicekey = integrador.servicekey
)
response = rpc.send_rpc(payload=payload.model_dump(exclude_none=True))
payload = response.get('payload')
hmac = response.get('hmac')
if not payload or not hmac:
message = f"Resposta RPC inválida: {response}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
response_data = rpc.get_payload(hmac, payload)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, f"Título a receber criado: {response_data.get('id_titulo')}", OpStatus.OK))
return response_data.get('id_titulo')
except Exception as e:
print(f"Erro ao enviar dados para título a receber: {e}", flush=True)
message = f"Erro ao enviar dados para fila de título a receber: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
def criar_nfe(pedido: TrayOrderRootResponse, banco: Banco) -> bool:
step_status = "NFe - CRIACAO"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE))
try:
nfe = NfeService(system_config, rd)
except Exception as e:
message = f"Erro ao inicializar NFeService: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CONFIGURAÇÃO DO CLIENTE",OpStatus.ACTIVE))
try:
config_tenant = banco.get_config_cliente(tenant=tenant)
if not config_tenant:
message = f"Configuração do cliente '{tenant}' não encontrada no banco de dados."
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
except Exception as e:
message = f"Erro ao obter configuração do cliente: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"MAPPING DOS DADOS",OpStatus.ACTIVE))
try:
payload = nfe._mapper_pedido_tray_to_nfe(pedido, config_tenant)
except Exception as e:
message = f"Erro ao mapear dados: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CREDENCIAIS DO INTEGRADOR",OpStatus.ACTIVE))
try:
integrador = banco.get_credenciais_servicos_integradores(integrador="nfe")
if not integrador:
message = f"Credenciais para integrador 'nfe' não encontradas no banco de dados."
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
except Exception as e:
message = f"Erro ao obter credenciais do integrador: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"ENVIANDO DADOS PARA FILA DE NFE (QUEUE)",OpStatus.ACTIVE))
try:
queue = SendQueueService(
system = integrador.system,
service = integrador.service,
amqps = integrador.amqps,
origin = origin,
serviceid = integrador.serviceid,
servicekey = integrador.servicekey
)
queue.send_queue(payload=payload.model_dump())
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Dados enviados para fila de NFe", OpStatus.OK))
return True
except Exception as e:
message = f"Erro ao enviar dados para fila de NFe: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
def atualizar_nfe_tray(id_pedido_tray: str, xml: str, banco: Banco) -> bool:
step_status = "NFe - ATUALIZACAO TRAY"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"Localizando dados no XML",OpStatus.ACTIVE))
try:
ns = {"nfe": "http://www.portalfiscal.inf.br/nfe"}
root = ET.fromstring(xml)
chave = root.find(".//nfe:chNFe", ns)
data_emissao = root.find(".//nfe:dhEmi", ns)
serie = root.find(".//nfe:serie", ns)
value = root.find(".//nfe:vNF", ns)
numero = root.find(".//nfe:nNF", ns)
chave = chave.text if chave is not None else None
data_emissao = data_emissao.text if data_emissao is not None else None
serie = serie.text if serie is not None else None
value = value.text if value is not None else None
numero = numero.text if numero is not None else None
if not all([chave, data_emissao, serie, value, numero]):
message = "Dados necessários não encontrados no XML"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
itens = root.findall(".//nfe:det", ns)
produtos = []
for item in itens:
cProd = item.find(".//nfe:cProd", ns)
xProd = item.find(".//nfe:xProd", ns)
cfop = item.find(".//nfe:CFOP", ns)
produtos.append({
"product_id" : cProd.text if cProd is not None else None,
"variation_id": xProd.text if xProd is not None else None,
"cfop" : cfop.text if cfop is not None else None
})
payload = {
"issue_date" : data_emissao[:10],
"number" : numero,
"serie" : serie,
"value" : value,
"key" : chave,
"link" : None,
"xml_danfe" : xml,
"ProductCfop": produtos
}
except Exception as e:
message = f"Erro ao extrair dados do XML da NFe: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
step_status = "NFe - ATUALIZACAO TRAY"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"Enviando dados para atualização do pedido no Tray",OpStatus.ACTIVE))
try:
api_tray = ApiTray(system_config=system_config, redis_client=rd)
api_tray.inserir_nfe_pedido(id_pedido=id_pedido_tray, nfe=payload)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Dados enviados para atualização do pedido no Tray", OpStatus.OK))
# return True
except Exception as e:
message = f"Erro ao enviar dados para atualização do pedido no Tray: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
# return False
step_status = "ATUALIZANDO NFe NO BANCO"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE))
try:
banco.update_pedido_nfe(id_pedido_tray = id_pedido_tray, status="Faturado", nfe_gerada = True,nfe_numero=numero,nfe_serie=serie)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "NFe atualizada no banco de dados", OpStatus.OK))
return True
except Exception as e:
message = f"Erro ao atualizar NFe no banco de dados: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return False
############################################################## < Fim logica > ##############################################################
def sendStatus(logger: LoggerExchange2):
global TaskCount, stats
TaskCount += 1
stats = metrics.get_metrics()
cpu_p, cores, load_1, load_5, load_15, m_total, m_used, m_free, m_pct, net_up, net_down, net_total, disk_usage = stats
logger.logIC2(realm = RealmType.STATUS, msg = (TaskCount, cpu_p, m_pct, disk_usage, net_total))
def tratar_recebimento_webhook_tray(recebimento: Recebimento):
logger.logMsg(f"Tratando recebimento do webhook Tray: {recebimento}")
if recebimento.data.scope_name != "order":
logger.logMsg(f"Escopo desconhecido: {recebimento.data.scope_name}")
return
logger.ic2_builder.dados.task_id = recebimento.data.scope_id
dados_tray = buscar_dados_tray(recebimento.data.scope_id)
if not dados_tray:
logger.logMsgError("Erro ao buscar dados do pedido no Tray. Verifique os logs para mais detalhes.")
return
step_status = "INICIANDO BANCO DE DADOS"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando conexão com o banco de dados", OpStatus.ACTIVE))
try:
banco = Banco(database = system_config.db_name,
user = system_config.db_user,
password = system_config.db_password,
host = system_config.db_host,
port = system_config.db_port)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Banco de dados inicializado com sucesso", OpStatus.OK))
except Exception as e:
message = f"Erro ao conectar ao banco de dados: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return
step_status = "INSERINDO PEDIDO NO BANCO CASO NÃO EXISTA"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando inserção do pedido no banco de dados caso não exista", OpStatus.ACTIVE))
try:
banco.upsert_pedido(id_pedido_tray=recebimento.data.scope_id, status=recebimento.data.act, tenant=tenant)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Pedido inserido no banco de dados com sucesso", OpStatus.OK))
except Exception as e:
message = f"Erro ao inserir pedido no banco de dados: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return
match recebimento.data.act:
case "insert":
cliente_id = criar_cliente(dados_tray, banco)
if not cliente_id:
return
step_status = "ATUALIZANDO CLIENTE NO PEDIDO (DB)"
logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, "Iniciando atualização do cliente no pedido", OpStatus.ACTIVE))
try:
banco.update_pedido_cliente(id_pedido_tray=recebimento.data.scope_id, codigo_cliente=cliente_id)
logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, "Cliente atualizado no pedido com sucesso", OpStatus.OK))
except Exception as e:
message = f"Erro ao atualizar cliente no pedido: {e}"
logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return
case "update":
if dados_tray.Order.status.upper() != "A ENVIAR":
logger.logMsgError(f"Status do pedido diferente do esperado {dados_tray.Order.status.upper()}")
nfe = criar_nfe(dados_tray, banco)
if not nfe:
return
step_status = "ATUALIZANDO ENVIADO PLUGNOTAS NO PEDIDO (DB)"
logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, "Atualizando pedido no banco", OpStatus.ACTIVE))
try:
banco.update_pedido_enviado_plugnotas(id_pedido_tray=recebimento.data.scope_id, enviado_plugnotas=True)
logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, "enviado_plugnotas atualizado no banco", OpStatus.OK))
except Exception as e:
message = f"Erro ao atualizar enviado_plugnotas no pedido: {e}"
logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return
case _:
logger.logMsg(f"Ação desconhecida: {recebimento.data.act}")
return
time.sleep(0.5)
return True
def tratar_webhook_plugnotas(recebimento: RecebimentoPlugnotas):
print(f"WebHook recebido do PlugNotas: {recebimento}", flush=True)
if recebimento.status.upper() != "CONCLUIDO":
logger.logMsgError(f"Status do webhook do PlugNotas diferente do esperado: {recebimento.status}")
return
id_pedido_tray = recebimento.idIntegracao.split("_")[1]
dados_tray = buscar_dados_tray(id_pedido_tray)
if not dados_tray:
logger.logMsgError(f"Erro ao buscar dados do pedido no Tray: {id_pedido_tray}")
return
step_status = "INICIANDO BANCO DE DADOS"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando conexão com o banco de dados", OpStatus.ACTIVE))
try:
banco = Banco(database = system_config.db_name,
user = system_config.db_user,
password = system_config.db_password,
host = system_config.db_host,
port = system_config.db_port)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Banco de dados inicializado com sucesso", OpStatus.OK))
except Exception as e:
message = f"Erro ao conectar ao banco de dados: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return
step_status = "BUSCANDO CHAVE DE ACESSO DA NFE"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Buscando chave de acesso da NFe no banco de dados", OpStatus.ACTIVE))
try:
xml = base64.b64decode(recebimento.xml_base64).decode()
ns = {"nfe": "http://www.portalfiscal.inf.br/nfe"}
root = ET.fromstring(xml)
chave_nfe = root.find(".//nfe:chNFe", ns)
numero = root.find(".//nfe:nNF", ns)
if chave_nfe is not None and numero is not None:
chave_nfe = chave_nfe.text
numero_nfe = numero.text
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, f"Chave de acesso da NFe encontrada: {chave_nfe}", OpStatus.ACTIVE))
else:
message = "Chave de acesso da NFe não encontrada no XML"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return
except Exception as e:
message = f"Erro ao buscar chave de acesso da NFe: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return
titulo = criar_titulo(dados_tray, banco, chave_nfe, numero_nfe)
if not titulo:
return
step_status = "ATUALIZANDO TITULO NO PEDIDO NO BANCO"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando atualização do titulo no pedido no banco", OpStatus.ACTIVE))
try:
banco.update_pedido_titulo(id_pedido_tray=id_pedido_tray, titulo_gerado=titulo)
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Titulo atualizado no pedido no banco com sucesso", OpStatus.OK))
except Exception as e:
print(f"Erro ao atualizar titulo no pedido no banco: {e}", flush=True)
message = f"Erro ao atualizar titulo no pedido no banco: {e}"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR))
return
step_status = "ATUALIZANDO NFE NO TRAY"
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando atualização da NFe no Tray", OpStatus.ACTIVE))
if not atualizar_nfe_tray(id_pedido_tray, xml):
logger.logMsgError("Erro ao atualizar NFe no Tray. Verifique os logs para mais detalhes.")
return
logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "NFe atualizada no Tray com sucesso", OpStatus.OK))
def callback(ch, method, properties, body):
# Controlador de mensagens do RabbitMQ: decodifica a mensagem e inicia o processo de clonagem.
global logger
sendStatus(logger)
logger.logMsg(f"Mensagem recebida: {body}")
# Confirma o recebimento da mensagem (ACK) para remover da fila
ch.basic_ack(delivery_tag=method.delivery_tag)
dado = json.loads(body)
hmac = dado['hmac']
payload = dado['payload']
if not hmac or not payload:
logger.logMsg("HMAC ou Payload inválido")
return
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=system_config.servicekey)
if not exchange.getPayload(hmac, payload):
logger.logMsg("Falha ao descifrar payload")
return
payload_dict = json.loads(exchange.payload)
if not payload_dict:
logger.logMsg("Payload inválido")
return
# Verifica qual modelo de recebimento é compativel -
PayloadModel = Recebimento | RecebimentoPlugnotas
adapter = TypeAdapter(PayloadModel)
try:
logger.logMsg(f"Validando payload recebido: {payload_dict}")
recebimento = adapter.validate_python(payload_dict)
if isinstance(recebimento, Recebimento):
logger.logMsg("Payload validado como Recebimento do webhook Tray")
tratar_recebimento_webhook_tray(recebimento)
elif isinstance(recebimento, RecebimentoPlugnotas):
logger.logMsg("Payload validado como Recebimento do webhook PlugNotas")
tratar_webhook_plugnotas(recebimento)
else:
logger.logMsgError("Modelo de recebimento fora do padrão")
return
except Exception as e:
logger.logMsgError(f"Erro ao converter recebimento \n {recebimento} \n {e}")
return
# --- Lógica de Inicialização do Script ---
# CONFIGURAÇÃO SERVIÇO REDIS
try:
rd = redis.Redis(host='redis', port=6379, decode_responses=True)
key = f"{system}-system_config-{tenant}"
if not rd.exists(key):
msg = f"Key '{key}' not found in Redis."
print(msg, flush=True)
time.sleep(5)
sys.exit(0)
except Exception as e:
print(f"Error connecting to Redis: A {e}", flush=True)
time.sleep(5)
sys.exit(0)
# VALIDA JSON
try:
configRaw = rd.get(key)
configJS = json.loads(configRaw)
except JSONDecodeError as e:
print(f"Error connecting to Redis: B {e}", flush=True)
time.sleep(5)
sys.exit(0)
except Exception as e:
print(f"Error REDIS GET KEY {key}: {e}", flush=True)
time.sleep(5)
sys.exit(0)
# VALIDA QUALIDADE DO JSON
try:
system_config = SystemConfig(**configJS)
except ValidationError as e:
print(f"Error connecting to Redis: C {e}", flush=True)
time.sleep(5)
sys.exit(0)
# Estruturas de dados para logging e telemetria
dados = Dados(
service_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"), # INICIO DO TRABALHO - FICA FIXO ATÉ FINAL DO TRABALHO
startup_date = startup_date,
task_id = "00000000", #secrets.token_hex(8).upper(),
service = service,
versionstr = versionstr,
origin = origin,
enterprise_id = system_config.enterprise_id,
enterprise_name = system_config.enterprise_name,
system = system
)
logger = LoggerExchange2(
ic2_builder = AppIC2Builder(dados),
amqps = system_config.ic2_url,
system = system_config.system,
service = service,
version = versionstr,
origin = origin,
client = tenant,
logType = "logs",
logLocal = True
)
try:
exchange = QueueExchange2(
amqps = system_config.amqps,
origin = origin,
system = system,
service = service,
version = versionstr,
# client = tenant,
callback= callback)
except Exception as e:
print(f"Erro conectando a fila: {e}", flush=True)
time.sleep(5)
sys.exit(0)
# Inicialização operacional
logger.logMsg(f"{system.upper()} - SERVIÇO DE RESERVAS v{version}")
# Inicia o consumo perpétuo da fila
exchange.start_consuming()

5
src/create_tables.py Normal file
View file

@ -0,0 +1,5 @@
from db.banco import Banco
db = Banco(database="innovar_amr_tray_controller",port=5432, user="softgo", password="[PASSWORD]", host="[IP_ADDRESS]")
db.create_tables()

284
src/db/banco.py Normal file
View file

@ -0,0 +1,284 @@
from datetime import datetime, timezone
from sqlalchemy import Column, DateTime, Integer, Numeric, String, Text, Boolean, create_engine
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
class CredenciaisServicosIntegradores(Base):
__tablename__ = 'credenciais_servicos_integradores'
__table_args__ = {'schema': 'controller'}
id_credenciais_servicos_integradores = Column(Integer, primary_key=True)
integrador = Column(String, nullable=False)
amqps = Column(String, nullable=False)
service = Column(String, nullable=False)
system = Column(String, nullable=False)
serviceid = Column(String, nullable=False)
servicekey = Column(String, nullable=False)
class ConfigTenant(Base):
__tablename__ = 'config_clientes'
__table_args__ = {'schema': 'controller'}
id_config_cliente = Column(Integer, primary_key=True)
tenant = Column(String, nullable=False)
cpf_cnpj = Column(String, nullable=False)
empresa_integracao = Column(String, nullable=False)
indexador_integracao = Column(String, nullable=False)
inscricao_estadual = Column(String)
usuario_sienge = Column(String, nullable=False)
token_sienge = Column(String, nullable=False)
oauth = Column(Boolean, nullable=False)
class ApropriacaoFinanceira(Base):
__tablename__ = 'apropriacao_financeira'
__table_args__ = {'schema': 'controller'}
id_apropriacao_financeira = Column(Integer, primary_key=True)
tenant = Column(String, nullable=False)
centro_custo = Column(Integer, nullable=False)
plano_financeiro = Column(String, nullable=False)
percentual_apropriacao = Column(Numeric, nullable=False)
class Pedido(Base):
__tablename__ = 'pedido'
__table_args__ = {'schema': 'controller'}
id_pedido_tray = Column(Integer, primary_key=True)
tenant = Column(String, nullable=False)
status = Column(String, nullable=False)
codigo_cliente = Column(String)
enviado_plugnotas = Column(Boolean, nullable=False)
nfe_gerada = Column(Boolean, nullable=False)
numero_nfe = Column(String)
serie_nfe = Column(String)
titulo_gerado = Column(Integer)
data_criacao = Column(DateTime)
data_atualizacao = Column(DateTime)
class Banco:
def __init__(self, host, port, user, password, database):
try:
self.engine = create_engine(
f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{database}",
echo = False,
future = True,
pool_pre_ping = True,
pool_recycle = 1800,
logging_name = "TrayController",
)
self.Session = sessionmaker(bind=self.engine)
self.session = self.Session()
except Exception as e:
print(f"Erro ao conectar ao banco de dados: {e}")
raise e
except SQLAlchemyError as e:
print(f"Erro ao conectar ao banco de dados: {e}")
raise e
finally:
self.session.close()
def create_tables(self):
try:
Base.metadata.create_all(self.engine)
except Exception as e:
print(f"Erro ao criar tabelas: {e}")
except SQLAlchemyError as e:
print(f"Erro ao criar tabelas: {e}")
finally:
self.session.close()
# MÉTODOS PARA GERENCIAMENTO DE PEDIDOS - AQUI PODEMOS IMPLEMENTAR MÉTODOS PARA INSERIR, ATUALIZAR E CONSULTAR PEDIDOS NO BANCO DE DADOS
def get_pedido(self, id_pedido_tray):
try:
pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first()
return pedido
except Exception as e:
print(f"Erro ao buscar pedido: {e}")
finally:
self.session.close()
def get_all_pedidos(self):
try:
pedidos = self.session.query(Pedido).all()
return pedidos
except Exception as e:
print(f"Erro ao buscar pedidos: {e}")
finally:
self.session.close()
def upsert_pedido(self, id_pedido_tray, status, tenant):
try:
pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first()
if not pedido:
pedido = Pedido(
id_pedido_tray = id_pedido_tray,
status = status,
tenant = tenant,
enviado_plugnotas = False,
nfe_gerada = False,
data_criacao = datetime.now(timezone.utc)
)
self.session.add(pedido)
else:
pedido.status = status
pedido.data_atualizacao = datetime.now(timezone.utc)
self.session.commit()
except Exception as e:
print(f"Erro ao inserir pedido: {e}", flush=True)
finally:
self.session.close()
def update_pedido_cliente(self, id_pedido_tray, codigo_cliente):
try:
pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first()
if pedido:
print(f"Atualizando pedido {id_pedido_tray} com cliente {codigo_cliente}", flush=True)
pedido.codigo_cliente = str(codigo_cliente)
pedido.data_atualizacao = datetime.now(timezone.utc)
self.session.commit()
except Exception as e:
print(f"Erro ao atualizar pedido: {e}", flush=True)
finally:
self.session.close()
def update_pedido_enviado_plugnotas(self, id_pedido_tray, enviado_plugnotas):
try:
pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first()
if pedido:
pedido.enviado_plugnotas = enviado_plugnotas
pedido.data_atualizacao = datetime.now(timezone.utc)
self.session.commit()
except Exception as e:
print(f"Erro ao atualizar pedido: {e}", flush=True)
finally:
self.session.close()
def update_pedido_nfe(self, id_pedido_tray, status, nfe_gerada, nfe_numero, nfe_serie):
try:
pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first()
if pedido:
pedido.status = status
pedido.nfe_gerada = nfe_gerada
pedido.nfe_numero = nfe_numero
pedido.nfe_serie = nfe_serie
pedido.data_atualizacao = datetime.now(timezone.utc)
self.session.commit()
except Exception as e:
print(f"Erro ao atualizar pedido: {e}", flush=True )
finally:
self.session.close()
def update_pedido_titulo(self, id_pedido_tray, titulo_gerado):
try:
pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first()
if pedido:
pedido.titulo_gerado = titulo_gerado
pedido.data_atualizacao = datetime.now(timezone.utc)
self.session.commit()
except Exception as e:
print(f"Erro ao atualizar pedido: {e}", flush=True)
finally:
self.session.close()
# MÉTODOS PARA GERENCIAMENTO DE CONFIGURAÇÕES - AQUI PODEMOS IMPLEMENTAR MÉTODOS PARA INSERIR, ATUALIZAR E CONSULTAR CONFIGURAÇÕES DE CLIENTES NO BANCO DE DADOS
def get_config_cliente(self, tenant):
try:
config = self.session.query(ConfigTenant).filter(ConfigTenant.tenant == tenant).first()
return config
except Exception as e:
print(f"Erro ao buscar configuração do cliente: {e}")
finally:
self.session.close()
def insert_config_cliente(self, tenant, cpf_cnpj, empresa_integracao, indexador_integracao, inscricao_estadual, usuario_sienge, token_sienge, oauth):
try:
config = ConfigTenant(
tenant = tenant,
cpf_cnpj = cpf_cnpj,
empresa_integracao = empresa_integracao,
indexador_integracao = indexador_integracao,
inscricao_estadual = inscricao_estadual,
usuario_sienge = usuario_sienge,
token_sienge = token_sienge,
oauth = oauth
)
self.session.add(config)
self.session.commit()
except Exception as e:
print(f"Erro ao inserir configuração do cliente: {e}")
finally:
self.session.close()
def update_config_cliente(self, tenant, cpf_cnpj, empresa_integracao, indexador_integracao, inscricao_estadual, usuario_sienge, token_sienge, oauth):
try:
config = self.session.query(ConfigTenant).filter(ConfigTenant.tenant == tenant).first()
if config:
config.cpf_cnpj = cpf_cnpj
config.empresa_integracao = empresa_integracao
config.indexador_integracao = indexador_integracao
config.inscricao_estadual = inscricao_estadual
config.usuario_sienge = usuario_sienge
config.token_sienge = token_sienge
config.oauth = oauth
self.session.commit()
except Exception as e:
print(f"Erro ao atualizar configuração do cliente: {e}")
finally:
self.session.close()
# MÉTODOS PARA GERENCIAMENTO DE APROPRIAÇÕES FINANCEIRAS - AQUI PODEMOS IMPLEMENTAR MÉTODOS PARA INSERIR, ATUALIZAR E CONSULTAR APROPRIAÇÕES FINANCEIRAS NO BANCO DE DADOS
def get_lista_apropriacao_financeira(self, tenant):
try:
apropriacao = self.session.query(ApropriacaoFinanceira).filter(
ApropriacaoFinanceira.tenant == tenant
).all()
return apropriacao
except Exception as e:
print(f"Erro ao buscar apropriação financeira: {e}")
finally:
self.session.close()
def insert_apropriacao_financeira(self, tenant, centro_custo, plano_financeiro, percentual_apropriacao):
try:
apropriacao = ApropriacaoFinanceira(
tenant = tenant,
centro_custo = centro_custo,
plano_financeiro = plano_financeiro,
percentual_apropriacao = percentual_apropriacao
)
self.session.add(apropriacao)
self.session.commit()
except Exception as e:
print(f"Erro ao inserir apropriação financeira: {e}")
finally:
self.session.close()
def update_apropriacao_financeira(self, tenant, centro_custo, plano_financeiro, percentual_apropriacao):
try:
apropriacao = self.session.query(ApropriacaoFinanceira).filter(
ApropriacaoFinanceira.tenant == tenant,
ApropriacaoFinanceira.centro_custo == centro_custo
).first()
if apropriacao:
apropriacao.plano_financeiro = plano_financeiro
apropriacao.percentual_apropriacao = percentual_apropriacao
self.session.commit()
except Exception as e:
print(f"Erro ao atualizar apropriação financeira: {e}")
finally:
self.session.close()
# MÉTODOS PARA GERENCIAMENTO DE CREDENCIAIS - AQUI PODEMOS IMPLEMENTAR MÉTODOS PARA INSERIR, ATUALIZAR E CONSULTAR CREDENCIAIS DE SERVIÇOS INTEGRADORES NO BANCO DE DADOS
def get_credenciais_servicos_integradores(self, integrador):
try:
credenciais = self.session.query(CredenciaisServicosIntegradores).filter(
CredenciaisServicosIntegradores.integrador == integrador
).first()
return credenciais
except Exception as e:
print(f"Erro ao buscar credenciais de serviços integradores: {e}")
finally:
self.session.close()

1
src/exchangeModels.py Symbolic link
View file

@ -0,0 +1 @@
../utils/exchangeModels.py

View file

@ -0,0 +1,102 @@
from pydantic import BaseModel
class EnderecoNfe(BaseModel):
logradouro : str | None = None
numero : str | None = None
bairro : str | None = None
codigoCidade : str | None = None
tipoLogradouro : str | None = None
estado : str | None = None
cep : str | None = None
class EmitenteNfe(BaseModel):
cpfCnpj : str | None = None
inscricaoEstadual: str | None = None
class DestinatarioNfe(BaseModel):
cpfCnpj : str | None = None
razaoSocial: str | None = None
email : str | None = None
endereco : EnderecoNfe
class ValorUnitarioNfe(BaseModel):
comercial : float | None = None
tributavel: float | None = None
class BaseCalculoNfe(BaseModel):
modalidadeDeterminacao: int | None = None
valor : float | None = None
quantidade : float | None = None
class IcmsNfe(BaseModel):
origem : str | None = None
cst : str | None = None
baseCalculo: BaseCalculoNfe
aliquota : float | None = None
valor : float | None = None
class PisNfe(BaseModel):
cst : str | None = None
baseCalculo: BaseCalculoNfe
aliquota : float | None = None
valor : float | None = None
class CofinsNfe(BaseModel):
cst : str | None = None
baseCalculo: BaseCalculoNfe
aliquota : float | None = None
valor : float | None = None
class TributosNfe(BaseModel):
icms : IcmsNfe
pis : PisNfe
cofins: CofinsNfe
class ItensNfe(BaseModel):
codigo : str | None = None
descricao : str | None = None
ncm : str | None = None
cest : str | None = None
cfop : str | None = None
valorUnitario: ValorUnitarioNfe
valor : float | None = None
tributos : TributosNfe
class PagamentosNfe(BaseModel):
aVista: bool | None = None
meio : str | None = None
valor : float | None = None
class TelefoneNfe(BaseModel):
ddd : str | None = None
numero: str | None = None
class ResponsavelTecnicoNfe(BaseModel):
cpfCnpj : str | None = None
nome : str | None = None
email : str | None = None
telefone: TelefoneNfe | None = None
class DadosNfe(BaseModel):
idIntegracao : str | None = None
presencial : bool | None = None
consumidorFinal : bool | None = None
natureza : str | None = None
emitente : EmitenteNfe
destinatario : DestinatarioNfe
itens : list[ItensNfe]
pagamentos : list[PagamentosNfe]
responsavelTecnico: ResponsavelTecnicoNfe | None = None
class FilaRetorno(BaseModel):
system : str | None = None
service : str | None = None
serviceid : str | None = None
servicekey: str | None = None
class EmitirNfe(BaseModel):
fila_retorno: FilaRetorno
fila : str | None = None
metodo : str | None = None
cliente : str | None = None
dados : DadosNfe

19
src/models/recebimento.py Normal file
View file

@ -0,0 +1,19 @@
from pydantic import BaseModel
from models.sg_tray_models.services_model import TrayWebhookPayload
class Metadata(BaseModel):
cliente : str
origem : str
timestamp: str
class Recebimento(BaseModel):
metadata: Metadata
data : TrayWebhookPayload
class RecebimentoPlugnotas(BaseModel):
id : str | None = None
status : str | None = None
documento : str | None = None
idIntegracao : str | None = None
emitente : str | None = None
xml_base64 : str | None = None

View file

@ -0,0 +1,117 @@
from pydantic import BaseModel
class Addresses(BaseModel):
type : str | None = None
streetName : str | None = None
number : str | None = None
complement : str | None = None
neighborhood : str | None = None
cityId : int | None = None
city : str | None = None
state : str | None = None
zipCode : str | None = None
class Phones(BaseModel):
number : str | None = None
main : bool | None = None
type : str | None = None
note : str | None = None
idd : str | None = None
class AddressesSpouse(BaseModel):
city : str | None = None
complement : str | None = None
neighborhood : str | None = None
number : str | None = None
streetName : str | None = None
zipCode : str | None = None
class Spouse(BaseModel) :
foreigner : str | None = None
internationalId : str | None = None
cpf : str | None = None
name : str | None = None
email : str | None = None
sex : str | None = None
civilStatus : str | None = None
birthDate : str | None = None
numberIdentityCard : str | None = None
issueDateIdentityCard: str | None = None
profession : str | None = None
nationality : str | None = None
birthPlace : str | None = None
fatherName : str | None = None
motherName : str | None = None
cellphoneNumber : str | None = None
businessPhone : str | None = None
company : str | None = None
addresses : AddressesSpouse | None = None
class NaturalPersonData(BaseModel):
name : str | None = None
email : str | None = None
birthDate : str | None = None
birthPlace : str | None = None
civilStatus : str | None = None
cpf : str | None = None
mailingAddress : str | None = None
licenseNumber : str | None = None
licenseIssuingBody : str | None = None
licenseIssueDate : str | None = None
fatherName : str | None = None
sex : str | None = None
issueDateIdentityCard : str | None = None
matrimonialRegime : str | None = None
marriageDate : str | None = None
issuingBody : str | None = None
nationality : str | None = None
numberIdentityCard : str | None = None
motherName : str | None = None
profession : str | None = None
spouse : Spouse | None = None
class Agents(BaseModel):
id : int | None = None
class LegalPersonData(BaseModel):
name : str | None = None
email : str | None = None
cityRegistrationNumber : str | None = None
cnaeNumber : str | None = None
cnpj : str | None = None
contactName : str | None = None
creaNumber : str | None = None
establishmentDate : str | None = None
fantasyName : str | None = None
note : str | None = None
site : str | None = None
shareCapital : float | None = None
stateRegistrationNumber : str | None = None
technicalManager : str | None = None
agents : list[Agents] | None = None
class FamilyIncome(BaseModel):
kinsName : str | None = None
kinship : str | None = None
incomeValue : float | None = None
observation : str | None = None
class PostCustomerSienge(BaseModel):
personType : str | None = None
foreigner : str | None = None
typeId : int | None = None
internationalId : str | None = None
subtypeIds : list[int] | None = None
addresses : list[Addresses] | None = None
phones : list[Phones] | None = None
naturalPersonData : NaturalPersonData | None = None
legalPersonData : LegalPersonData | None= None
familyIncome : list[FamilyIncome] | None= None
class Config(BaseModel):
subdominio: str | None = None
usuario : str
senha : str
tenant : str | None = None
cliente : str | None = None

26
src/send_queue.py Normal file
View file

@ -0,0 +1,26 @@
import json
from Exchange2 import QueueExchange2
version = "1.0.0" # Define the version of your service
class SendQueueService:
def __init__(self,system: str, service: str, amqps: str, origin: str, serviceid: str, servicekey: str):
try:
self.service = service
self.exchange = QueueExchange2(amqps = amqps,
origin = origin,
system = system,
service = service,
version = version)
self.exchange.setCrypto(serviceid, servicekey)
except Exception as e:
raise e
def send_queue(self, payload: dict):
try:
self.exchange.setPayload(json.dumps(payload))
self.exchange.sendMsg(message=json.dumps({"queue": "OK"}), service=self.service)
return {"status": "success", "message": "Task sent successfully"}
except Exception as e:
return {"status": "error", "message": str(e)}

37
src/send_rpc.py Normal file
View file

@ -0,0 +1,37 @@
import json
from Exchange2 import RPCExchange2
version = "1.0.0" # Define the version of your service
class SendRPCService:
def __init__(self, system: str, service: str, amqps: str, origin: str, serviceid: str, servicekey: str):
try:
self.max_timeout = 15
self.exchange = RPCExchange2(amqps = amqps,
origin = origin,
system = system,
service = service,
version = version)
print(f"Conectando ao RabbitMQ em {amqps}...", flush=True)
print(f"system: {system}, service: {service}, origin: {origin}", flush=True)
self.exchange.setCrypto(serviceid, servicekey)
except Exception as e:
raise e
def send_rpc(self, payload: dict):
try:
self.exchange.setPayload(json.dumps(payload))
response = self.exchange.callRPC(message=json.dumps({"task": "OK"}), timeout=self.max_timeout)
return json.loads(response)
except Exception as e:
raise e
def get_payload(self, hmac, payload):
try:
if self.exchange.getPayload(hmac, payload):
return json.loads(self.exchange.payload)
else:
raise ValueError("Falha ao descifrar payload")
except Exception as e:
raise e

85
src/services/cliente.py Normal file
View file

@ -0,0 +1,85 @@
from models.sienge.services_models import *
from models.sg_tray_models.services_model import TrayOrderRootResponse
from appIC2Builder import SystemConfig
from api.tray import ApiTray
from db.banco import ConfigTenant
from pydantic import ValidationError
import redis
class ClienteService:
def __init__(self, config : SystemConfig, redis_client : redis.Redis):
self.config = config
self.api_tray = ApiTray(redis_client=redis_client, system_config=config)
def _mapper_tray_to_sienge_cliente(self, pedido : TrayOrderRootResponse, config_tenant: ConfigTenant) -> PostCustomerSienge:
enderecos : list[Addresses] = []
try:
for endereco in pedido.Order.Customer.CustomerAddresses:
enderecos.append(Addresses(
cityId = None,
city = endereco.CustomerAddress.city,
state = endereco.CustomerAddress.state,
neighborhood = endereco.CustomerAddress.neighborhood,
number = endereco.CustomerAddress.number,
complement = endereco.CustomerAddress.complement,
streetName = endereco.CustomerAddress.address,
zipCode = endereco.CustomerAddress.zip_code,
type = 'C' if endereco.CustomerAddress.description == "Comercial" else 'R'
))
except ValidationError as e:
print(f"Erro ao mapear endereços: {e}",flush=True)
raise e
try:
telefones = [Phones(
type = "CE",
number = pedido.Order.Customer.cellphone,
main = True
)]
except ValidationError as e:
print(f"Erro ao mapear telefones: {e}",flush=True)
raise e
try:
if pedido.Order.Customer.cpf is not None and pedido.Order.Customer.cpf != "":
cliente = PostCustomerSienge(
addresses = enderecos,
phones = telefones,
naturalPersonData = NaturalPersonData(
name = pedido.Order.Customer.name,
cpf = pedido.Order.Customer.cpf,
email = pedido.Order.Customer.email,
birthDate = pedido.Order.Customer.birth_date,
sex = 'M' if pedido.Order.Customer.gender == "0" else 'F',
numberIdentityCard=pedido.Order.Customer.rg
)
)
elif pedido.Order.Customer.cnpj is not None and pedido.Order.Customer.cnpj != "":
cliente = PostCustomerSienge(
addresses = enderecos,
phones = telefones,
legalPersonData = LegalPersonData(
name = pedido.Order.Customer.company_name,
email = pedido.Order.Customer.email,
cnpj = pedido.Order.Customer.cnpj,
stateRegistrationNumber = pedido.Order.Customer.state_inscription
)
)
else:
raise ValueError("Cliente sem CPF ou CNPJ")
return {
"config": {
"subdominio": "s8psasistemas", #config_tenant.tenant if not config_tenant.oauth else None,
"usuario" : config_tenant.usuario_sienge,
"senha" : config_tenant.token_sienge,
"tenant" : config_tenant.tenant if config_tenant.oauth else None,
"cliente" : config_tenant.tenant
},
"cliente": cliente.model_dump()
}
except ValidationError as e:
print(f"Erro ao mapear cliente: {e}",flush=True)
raise e

198
src/services/nfe.py Normal file
View file

@ -0,0 +1,198 @@
from models.plugnotas.services_model import DadosNfe
from models.sg_tray_models.services_model import TrayOrderRootResponse
from models.plugnotas.services_model import *
from appIC2Builder import SystemConfig
from db.banco import ConfigTenant
from api.tray import ApiTray
from api.publicas import busca_cep
import redis
class NfeService:
def __init__(self, config : SystemConfig, redis : redis.Redis):
self.config = config
self.api_tray = ApiTray(redis_client=redis, system_config=config)
self.meios_pagamento_plugnotas = {
"01" : "Dinheiro",
"02" : "Cheque",
"03" : "Cartão de Crédito",
"04" : "Cartão de Débito",
"05" : "Cartão da Loja (Private Label), Crediário Digital, Outros Crediários",
"10" : "Vale Alimentação",
"11" : "Vale Refeição",
"12" : "Vale Presente",
"13" : "Vale Combustível",
"14" : "Duplicata Mercantil",
"15" : "Boleto Bancário",
"16" : "Depósito Bancário",
"17" : "Pagamento Instantâneo (PIX) - Dinâmico",
"18" : "TED (Transferência Eletrônica Disponível)",
"19" : "Programa de fidelidade, Cashback, Crédito Virtual",
"20" : "Pagamento Instantâneo (PIX) - Estático",
"21" : "Crédito em Loja",
"22" : "Pagamento Eletrônico não Informado - falha de hardware do sistema emissor",
"23" : "Pagamento Instantâneo (PIX) - Automático",
"24" : "TEF - (Book Transfer)",
"90" : "Sem pagamento",
"91" : "Pagamento Posterior",
"99" : "Outros"
}
def localiza_tipo_logradouro(self, logradouro : str) -> str:
match logradouro.split(" ")[0].upper():
case "AVENIDA":
return "Avenida"
case "ALAMEDA":
return "Alameda"
case "CHACARA":
return "Chácara"
case "COLONIA":
return "Colônia"
case "CONDOMINIO":
return "Condomínio"
case "EQNP":
return "Eqnp"
case "ESTANCIA":
return "Estância"
case "ESTRADA":
return "Estrada"
case "FAZENDA":
return "Fazenda"
case "PRACA":
return "Praça"
case "PROLONGAMENTO":
return "Prolongamento"
case "RODOVIA":
return "Rodovia"
case "RUA":
return "Rua"
case "SITIO":
return "Sítio"
case "TRAVESSA":
return "Travessa"
case "VICINAL":
return "Vicinal"
case _:
return "Rua"
def localiza_meio_pagamento(self, pagamento : str) -> str:
for key, value in self.meios_pagamento_plugnotas.items():
if pagamento.upper() in value.upper():
return key
return "99"
def _mapper_pedido_tray_to_nfe(self, pedido_tray : TrayOrderRootResponse, config_tenant : ConfigTenant) -> EmitirNfe:
try:
emitente_nfe = EmitenteNfe(
cpfCnpj = config_tenant.cpf_cnpj,
inscricaoEstadual = config_tenant.inscricao_estadual
)
cep_info = busca_cep(pedido_tray.Order.Customer.zip_code)
if 'erro' in cep_info:
raise Exception(f"Erro ao buscar CEP: {cep_info['erro']}")
endereco_destinatario = EnderecoNfe(
bairro = cep_info['bairro'],
cep = cep_info['cep'],
codigoCidade = cep_info['ibge'],
estado = cep_info['uf'],
logradouro = cep_info['logradouro'],
numero = pedido_tray.Order.Customer.number,
tipoLogradouro = self.localiza_tipo_logradouro(cep_info['logradouro'])
)
destinatario_nfe = DestinatarioNfe(
cpfCnpj = pedido_tray.Order.Customer.cpf if pedido_tray.Order.Customer.cpf else pedido_tray.Order.Customer.cnpj,
razaoSocial = pedido_tray.Order.Customer.name,
email = pedido_tray.Order.Customer.email,
endereco = endereco_destinatario
)
itens_nfe = list[ItensNfe]()
for item in pedido_tray.Order.ProductsSold:
item_nfe = ItensNfe(
codigo = item.ProductsSold.product_id,
descricao = item.ProductsSold.name,
ncm = item.ProductsSold.ncm,
cfop = "", # Implementar
valorUnitario = ValorUnitarioNfe(
comercial = item.ProductsSold.price,
tributavel = item.ProductsSold.price
),
valor = item.ProductsSold.price,
tributos = TributosNfe(
icms=IcmsNfe(
origem = "0", # Implementar
cst = "00", # Implementar
baseCalculo=BaseCalculoNfe(
modalidadeDeterminacao = 0,
valor = 0,
quantidade = 0
),
aliquota = 0,
valor = 0
),
pis=PisNfe(
cst="99", # Implementar
baseCalculo=BaseCalculoNfe(
modalidadeDeterminacao = 0,
valor = 0,
quantidade = 0
),
aliquota = 0,
valor = 0
),
cofins=CofinsNfe(
cst="07", # Implementar
baseCalculo=BaseCalculoNfe(
modalidadeDeterminacao = 0,
valor = 0,
quantidade = 0
),
aliquota = 0,
valor = 0
)
)
)
itens_nfe.append(item_nfe)
pagamentos_nfe = list[PagamentosNfe]()
for pagamento in pedido_tray.Order.Payment:
pagamento_nfe = PagamentosNfe(
meio = self.localiza_meio_pagamento(pagamento.Payment.method),
valor = pagamento.Payment.value
)
pagamentos_nfe.append(pagamento_nfe)
dados_nfe = DadosNfe(
idIntegracao = f"PE_{pedido_tray.Order.id}",
emitente = emitente_nfe,
destinatario = destinatario_nfe,
itens = itens_nfe,
pagamentos = pagamentos_nfe
)
fila_retorno = FilaRetorno(
service = self.config.service,
serviceid = self.config.serviceid,
servicekey = self.config.servicekey,
system = self.config.system
)
nfe = EmitirNfe(
fila_retorno = fila_retorno,
fila = "nfe",
metodo = "post",
cliente = self.config.client,
dados = dados_nfe
)
return nfe
except Exception as e:
print(e ,flush=True)
raise Exception(f"Erro ao mapear pedido: {e}")

View file

@ -0,0 +1,51 @@
from models.servico_titulos_receber.contracts_Tittles import ReceivableBillPayload, SiengeCredentials
from models.servico_titulos_receber.tittle_model import PostAccountsReceivableReceivableBills, FinancialAppropriations
from models.sg_tray_models.services_model import TrayOrderRootResponse
from appIC2Builder import SystemConfig
from db.banco import ConfigTenant, ApropriacaoFinanceira, Pedido
class TituloReceberService:
def __init__(self, config: SystemConfig):
self.config = config
def map_tray_order_to_bill_data(self, tray_order : TrayOrderRootResponse, config_tenant: ConfigTenant, apropriacao: list[ApropriacaoFinanceira], pedido: Pedido, chave_nfe: str, numero_nfe: str) -> ReceivableBillPayload:
credenciais = SiengeCredentials(
subdomain = "s8psasistemas", #config_tenant.tenant,
username = config_tenant.usuario_sienge,
password = config_tenant.token_sienge
)
apropriacoes = [FinancialAppropriations(
costCenterId = item.centro_custo,
paymentCategoryId = item.plano_financeiro,
percentage = item.percentual_apropriacao,
amount = tray_order.Order.total
) for item in apropriacao]
bill_data = PostAccountsReceivableReceivableBills(
companyId = config_tenant.empresa_integracao,
customerId = pedido.codigo_cliente,
document = "NF",
documentNumber = numero_nfe,
issueDate = tray_order.Order.date,
competenceDate = tray_order.Order.date,
totalInvoiceAmount = tray_order.Order.total,
installmentsNumber = 1,#tray_order.Order.installment,
accountingDate = tray_order.Order.date,
indexerId = config_tenant.indexador_integracao,
baseDate = tray_order.Order.date,
dueDate = tray_order.Order.date,
notes = f"Titulo referente ao pedido {tray_order.Order.id} da plataforma Tray. Chave de acesso da NF-e: {chave_nfe}",
financialAppropriations = apropriacoes,
installments = None
)
integrador = ReceivableBillPayload(
tenant=self.config.client,
bill_data=bill_data,
credentials=credenciais
)
return integrador

201
src/teste.py Normal file
View file

@ -0,0 +1,201 @@
from models.sienge.services_models import Config
from models.plugnotas.services_model import *
# from api.tray import ApiTray
import json, redis
from models.sienge.services_models import *
from models.sg_tray_models.services_model import TrayOrderRootResponse
from send_rpc import SendRPCService
from pydantic import ValidationError
# from api.publicas import busca_cep
# rd = redis.Redis(host='localhost', port=6388, decode_responses=True)
# with open("modelo_envio_plugnotas.json", "r") as f:
# payload = json.load(f)
# emitir_nfe = EmitirNfe(**payload)
# print(emitir_nfe.model_dump_json(indent=4))
# api_tray = ApiTray(url='https://1225878.commercesuite.com.br',
# consumer_key='62598c0d55e7febda429d69a134dad3ac1adadc895e697e62df0ae214df62790',
# consumer_secret='7502e1994fd19108c2fa73a08559a8c76abc6e96a067057848152c78ec38ec66',
# code='beee2e6c9af8820622a72830862ca0f2c04ebc3859eed19ea6cffa799b938da4',
# redis_client=rd,
# service='controller',
# system='integrador',
# refresh_token=None)
# print(api_tray.get_pedido_completo(id_pedido=16101).model_dump())
# ceps = [
# "01001000", "01310100", "02020000", "03150000", "04567000",
# "05010000", "06050000", "07210000", "08021000", "09530000",
# "11035000", "12070000", "13083000", "14025000", "15015000",
# "16050000", "17012000", "18095000", "19061000", "19990000",
# "20040000", "20550000", "21040000", "22070002", "23050000",
# "24030000", "25010000", "26020000", "27035000", "28013000",
# "25555000", "26530000", "27280000", "27510000", "28155000",
# "28470000", "28625000", "28735000", "28890000", "28950000",
# "30110000", "30550000", "31030000", "32010000", "33025000",
# "34010000", "35020000", "36015000", "37010000", "38010000",
# "39010000", "34120000", "35501000", "36570000", "37540000",
# "38500000", "39400000", "39803000", "39950000", "39700000",
# "40015000", "41020000", "42010000", "43050000", "44015000",
# "45025000", "46010000", "47020000", "48015000", "48950000",
# "40170000", "41230000", "42500000", "43570000", "44580000",
# "45530000", "46570000", "47520000", "48550000", "48760000",
# "80010000", "80530000", "81050000", "82020000", "83005000",
# "84010000", "85012000", "86020000", "87013000", "88010000",
# "89010000", "80050100", "81560000", "83203000", "84266000",
# "85200000", "86200000", "87210000", "88220000", "87990000",
# "72800010", "74015000", "74120090", "74450000", "74810100",
# "75020000", "75503000", "75800000", "75901000", "76010000",
# "76300000", "73710000", "74230000", "74553000", "74934000",
# "75110000", "75690000", "76190000", "76400000", "76780000",
# "90010000", "91020000", "92010000", "93020000", "94010000",
# "95020000", "96015000", "97010000", "98020000", "99010000",
# "90550000", "91530000", "92510000", "93520000", "94560000",
# "95570000", "96508000", "97510000", "98560000", "99520000",
# "88010000", "88101000", "88220000", "88301000", "88400000",
# "88501000", "88600000", "88701000", "88805000", "88901000",
# "89010000", "89107000", "89201000", "89301000", "89460000",
# "89520000", "89600000", "89700000", "89801000", "89990000",
# "50030000", "51020000", "52010000", "53020000", "54010000",
# "55020000", "56010000", "56500000", "56750000", "56990000",
# "50710000", "51230000", "52250000", "53210000", "54280000",
# "55295000", "56200000", "56650000", "56890000", "56950000",
# "60020000", "60530000", "61010000", "62010000", "63020000",
# "60175000", "60630000", "61150000", "62110000", "63180000",
# "60250000", "60744000", "61200000", "62200000", "63260000",
# "60330000", "60860000", "61350000", "62320000", "63990000"
# ]
# for cep in ceps:
# print(f"Buscando CEP: {cep}")
# print(busca_cep(cep))
# print("-" * 50)
# meios_pagamento_plugnotas = {
# "01" : "Dinheiro",
# "02" : "Cheque",
# "03" : "Cartão de Crédito",
# "04" : "Cartão de Débito",
# "05" : "Cartão da Loja (Private Label), Crediário Digital, Outros Crediários",
# "10" : "Vale Alimentação",
# "11" : "Vale Refeição",
# "12" : "Vale Presente",
# "13" : "Vale Combustível",
# "14" : "Duplicata Mercantil",
# "15" : "Boleto Bancário",
# "16" : "Depósito Bancário",
# "17" : "Pagamento Instantâneo (PIX) - Dinâmico",
# "18" : "TED (Transferência Eletrônica Disponível)",
# "19" : "Programa de fidelidade, Cashback, Crédito Virtual",
# "20" : "Pagamento Instantâneo (PIX) - Estático",
# "21" : "Crédito em Loja",
# "22" : "Pagamento Eletrônico não Informado - falha de hardware do sistema emissor",
# "23" : "Pagamento Instantâneo (PIX) - Automático",
# "24" : "TEF - (Book Transfer)",
# "90" : "Sem pagamento",
# "91" : "Pagamento Posterior",
# "99" : "Outros"
# }
# def localiza_meio_pagamento(pagamento : str) -> str:
# for key, value in meios_pagamento_plugnotas.items():
# if pagamento.upper() in value.upper():
# return key
# return "99"
# print(localiza_meio_pagamento("PIX"))
def _mapper_tray_to_sienge_cliente(pedido : TrayOrderRootResponse) -> PostCustomerSienge:
enderecos : list[Addresses] = []
try:
for endereco in pedido.Order.Customer.CustomerAddresses:
enderecos.append(Addresses(
cityId = None,
city = endereco.CustomerAddress.city,
state = endereco.CustomerAddress.state,
neighborhood = endereco.CustomerAddress.neighborhood,
number = endereco.CustomerAddress.number,
complement = endereco.CustomerAddress.complement,
streetName = endereco.CustomerAddress.address,
zipCode = endereco.CustomerAddress.zip_code,
type = 'R'
))
except ValidationError as e:
print(f"Erro ao mapear endereços: {e}")
raise e
try:
telefones = [Phones(
type = "CE",
number = pedido.Order.Customer.cellphone,
main = True
)]
except ValidationError as e:
print(f"Erro ao mapear telefones: {e}")
raise e
try:
if pedido.Order.Customer.cpf is not None and pedido.Order.Customer.cpf != "":
cliente = PostCustomerSienge(
addresses = enderecos,
phones = telefones,
naturalPersonData = NaturalPersonData(
name = pedido.Order.Customer.name,
cpf = pedido.Order.Customer.cpf,
email = pedido.Order.Customer.email,
birthDate = pedido.Order.Customer.birth_date,
sex = 'M' if pedido.Order.Customer.gender == "0" else 'F',
numberIdentityCard=pedido.Order.Customer.rg
)
)
elif pedido.Order.Customer.cnpj is not None and pedido.Order.Customer.cnpj != "":
pass
else:
pass
return cliente
except ValidationError as e:
print(f"Erro ao mapear cliente: {e}")
raise e
with open("pedido_completo.json", "r") as f:
payload = json.load(f)
pedido = TrayOrderRootResponse(**payload)
cliente = _mapper_tray_to_sienge_cliente(pedido).model_dump()
config = Config(
subdominio = "s8psasistemas",
usuario = "s8psasistemas-midd",
senha = "fq3LdZzvTJq8u8oeASFhVkYV20NIac41",
tenant = None,
cliente = "s8psasistemas"
).model_dump()
payload = {
"cliente" : cliente,
"config" : config
}
with open("payload.json", "w") as f:
json.dump(payload, f, indent=4)
exchange = SendRPCService(
system = "integrador_clientes",
service = "controller",
amqps = "amqps://integracao-vendas:R2E2xV4HHQYSjvlIOPeoA@tall-cyan-dog.rmq4.cloudamqp.com/integracao-vendas",
origin = "pc-balt",
serviceid = "98765432109876543210987654321098",
servicekey = "ZYXWVUTSRQPONMLKJIHGFEDCBA654321"
)
print(exchange.send_rpc(payload))

14
src/testeNfe.py Normal file
View file

@ -0,0 +1,14 @@
from send_queue import SendQueueService
import json
with open("modelo_webhook_plugnotas.json", "r") as f:
payload = json.load(f)
queue_service = SendQueueService(amqps = "amqps://integracao-vendas:R2E2xV4HHQYSjvlIOPeoA@tall-cyan-dog.rmq4.cloudamqp.com/integracao-vendas",
service = "controller",
system = "innovar_amr_tray",
origin = "pc_balt",
serviceid = "G7q7VkP587Kf",
servicekey = "EImD4TXc1PIi4V8jbkRapyhv9veqlg7Q")
print(queue_service.send_queue(payload))

6
webhook.json Normal file
View file

@ -0,0 +1,6 @@
{
"scope_name": "order",
"scope_id": "16527",
"seller_id": "1225878",
"act": "insert"
}