Com o intuito de otimizar o processo de consulta de estoque, foi desenvolvido um sistema que integra com o ERP TOTVS-Protheus, permitindo consultas rápidas por código de produto, visualização de saldos, lotes, empenhos e movimentações em tempo real.
Busca instantânea de produtos pelo código, com informações detalhadas de estoque, lotes e movimentações.
Relatório completo de posição de estoque analítico com exportação para Excel formatado.
Visualização segmentada por almoxarifado, MP moído, MP fábrica e produto acabado.
Armazena as últimas consultas no navegador para acesso rápido aos produtos pesquisados.
Visualização de lotes com filtros por vencidos, próximos ao vencimento e ativos.
Exporta dados de saldo analítico para planilha Excel formatada com cabeçalhos e estilos profissionais.
| Armazém | Saldo Atual | Disponível | Reservado |
|---|---|---|---|
| 01 - Almoxarifado | 500 | 450 | 50 |
| 04 - Prod. Acabado | 734 | 537 | 73 |
| 11 - MP Moído | 0 | 0 | 0 |
"""
Trimplas - API Flask para consulta de produtos Protheus.
"""
import logging
from flask import Flask, render_template, request, jsonify, send_from_directory, Response
from flask_cors import CORS
from datetime import datetime
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.config import HOST, PORT, LOG_LEVEL
from src.db import buscar_info_produto, formatar_resposta, buscar_saldo_analitico, gerar_excel_matr260
app = Flask(__name__, template_folder='frontend', static_folder='frontend/static')
CORS(app)
@app.route("/")
def index():
"""Pagina principal."""
return render_template('index.html')
@app.route("/api/health")
def health():
return jsonify({
"status": "online",
"servidor": f"http://{HOST}:{PORT}",
"timestamp": datetime.now().isoformat()
})
@app.route("/api/produto/<codigo>")
def api_buscar_produto(codigo):
"""Busca informacoes de um produto."""
if not codigo or not codigo.strip():
return jsonify({"erro": "Codigo obrigatorio"}), 400
try:
dados = buscar_info_produto(codigo.strip())
if not dados["produto"]:
return jsonify({"erro": "Produto nao encontrado"}), 404
return jsonify({
"sucesso": True,
"codigo": codigo,
"dados": dados,
"timestamp": datetime.now().isoformat()
})
except Exception as e:
return jsonify({"erro": str(e)}), 500
@app.route("/api/matr260")
def api_matr260():
"""Reproduz o relatorio MATR260 - Posicao de Estoque Analitico."""
filial = request.args.get("filial", "01")
armazem = request.args.get("armazem", "")
saldo_tipo = request.args.get("saldo_tipo", "A")
if not armazem:
return jsonify({"erro": "Armazem obrigatorio"}), 400
registros = buscar_saldo_analitico(filial, armazem, saldo_tipo)
return jsonify({
"sucesso": True,
"filial": filial,
"armazem": armazem,
"registros": registros,
"total_registros": len(registros),
"timestamp": datetime.now().isoformat()
})
@app.route("/api/matr260/excel")
def api_matr260_excel():
"""Exporta relatorio MATR260 para Excel formatado."""
filial = request.args.get("filial", "01")
armazem = request.args.get("armazem", "")
registros = buscar_saldo_analitico(filial, armazem, "A")
excel_bytes = gerar_excel_matr260(registros, filial, armazem, "A", "")
return Response(
excel_bytes,
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
if __name__ == "__main__":
logger.info(" Trimplas - Consulta de Produtos")
logger.info(f" Interface: http://{HOST}:{PORT}/")
app.run(host=HOST, port=PORT, debug=False, threaded=True)
"""
Modulo de conexao e consultas ao banco Protheus.
Tabelas: SB1 (Produtos), SB2 (Saldos), SB8 (Lotes),
SC2 (Empenhos), SD3 (Movimentacoes)
"""
import pyodbc
import logging
from contextlib import contextmanager
from typing import Optional, List, Dict, Any
from src.config import DB_CONFIG
def _build_connection_string():
return (
"DRIVER={SQL Server};"
f"SERVER={DB_CONFIG['server']};"
f"DATABASE={DB_CONFIG['database']};"
f"UID={DB_CONFIG['username']};"
f"PWD={DB_CONFIG['password']};"
"Connection Timeout=30;"
)
@contextmanager
def get_connection():
"""Context manager para conexao com banco Protheus."""
conn = None
try:
conn = pyodbc.connect(_build_connection_string())
yield conn
finally:
if conn:
conn.close()
def _execute_query(query, params=None):
import pandas as pd
with get_connection() as conn:
df = pd.read_sql(query, conn, params=params)
df = df.astype(object).where(pd.notnull(df), None)
return df.to_dict('records')
def buscar_produto(codigo):
"""Busca informacoes basicas do produto (SB1010)."""
query = """
SELECT TOP 1
LTRIM(RTRIM(B1_COD)) AS CODIGO,
LTRIM(RTRIM(B1_DESC)) AS DESCRICAO,
LTRIM(RTRIM(B1_UM)) AS UNIDADE,
LTRIM(RTRIM(B1_GRUPO)) AS GRUPO
FROM SB1010
WHERE B1_COD = ? AND D_E_L_E_T_ = ''
"""
resultados = _execute_query(query, (codigo,))
return resultados[0] if resultados else None
def buscar_saldos(codigo):
"""Busca saldos do produto por armazem (SB2010)."""
query = """
SELECT
LTRIM(RTRIM(B2_LOCAL)) AS ARMAZEM,
B2_QATU AS SALDO_ATUAL,
B2_RESERV AS RESERVADO,
B2_EMPENH AS EMPENHADO,
(B2_QATU - B2_RESERV - B2_EMPENH) AS DISPONIVEL
FROM SB2010
WHERE B2_COD = ? AND D_E_L_E_T_ = ''
ORDER BY B2_LOCAL
"""
return _execute_query(query, (codigo,))
<!-- Frontend principal da aplicacao de consulta -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Trimplas - Consulta de Produtos</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="app">
<!-- HEADER com busca e acoes -->
<header class="header">
<div class="header-brand">
<div class="brand-text">
<span class="brand-name">Trimplas</span>
<span class="brand-subtitle">Perfilados Plasticos</span>
</div>
</div>
<!-- Filtro por Armazem -->
<form class="search-form" id="search-form">
<div class="warehouse-filter">
<button type="button" data-armazem="">Exibir todos</button>
<button type="button" data-armazem="01">01 - Almoxarifado</button>
<button type="button" data-armazem="04">04 - Produto Acabado</button>
</div>
<!-- Input de busca -->
<div class="search-wrapper">
<input type="text" id="codigo"
placeholder="Buscar por codigo do produto...">
</div>
<button type="submit" class="btn-search">Buscar</button>
</form>
<!-- Acoes: Saldo Analitico, Historico, Status -->
<div class="header-actions">
<button id="btn-matr260">Saldo Analitico</button>
<button id="btn-history">Histórico</button>
<div class="status-pill">
<span class="status-dot"></span>
<span class="status-text">Online</span>
</div>
</div>
</header>
<!-- WELCOME STATE -->
<div class="welcome-state" id="welcome-state">
<h1>Consulta de Produtos</h1>
<p>Digite o codigo do produto no campo de busca</p>
</div>
<!-- PRODUCT VIEW -->
<div class="product-view" id="product-view">
<div class="product-header">
<h2 id="product-name">--</h2>
<button class="btn btn-success" id="btn-export">
Exportar Excel
</button>
</div>
<!-- Sumario de Estoques -->
<div class="stock-summary">
<div class="summary-card">
<span>Estoque Total</span>
<strong id="summary-total">0,00</strong>
</div>
<!-- ... demais cards ... -->
</div>
<!-- Abas: Saldos | Lotes | Empenhos | Movimentacoes -->
<div class="tabs">
<button class="tab active">Saldos</button>
<button class="tab">Lotes</button>
<button class="tab">Empenhos</button>
<button class="tab">Movimentações</button>
</div>
</div>
<!-- LOADING STATE -->
<div class="loading-state" id="loading-state">
Carregando...
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>
const API_BASE = '';
let dadosAtuais = null;
let codigoAtual = null;
let armazemSelecionado = '';
let searchHistory = JSON.parse(localStorage.getItem('searchHistory') || '[]');
function formatarNumero(valor) {
if (valor === null || valor === undefined) return '0,00';
const num = parseFloat(valor);
return isNaN(num) ? '0,00' :
num.toLocaleString('pt-BR', { minimumFractionDigits: 2 });
}
function showState(state) {
document.getElementById('welcome-state').style.display =
state === 'welcome' ? 'flex' : 'none';
document.getElementById('product-view').style.display =
state === 'product' ? 'block' : 'none';
document.getElementById('loading-state').style.display =
state === 'loading' ? 'flex' : 'none';
}
async function buscarProduto(codigo) {
codigo = codigo.toUpperCase().trim();
if (!codigo) return;
showState('loading');
try {
const response = await fetch(
`${API_BASE}/api/produto/${encodeURIComponent(codigo)}`
);
const data = await response.json();
if (!data.sucesso || !data.dados.produto) {
showState('error');
return;
}
dadosAtuais = data;
codigoAtual = codigo;
atualizarUI();
showState('product');
} catch (error) {
showState('error');
}
}
function atualizarUI() {
const p = dadosAtuais.dados.produto;
const saldos = dadosAtuais.dados.saldos || [];
document.getElementById('product-name').textContent = p.DESCRICAO;
document.getElementById('product-code').textContent = p.CODIGO;
const total = saldos.reduce((acc, s) => acc + (parseFloat(s.SALDO_ATUAL) || 0), 0);
const disp = saldos.reduce((acc, s) => acc + (parseFloat(s.DISPONIVEL) || 0), 0);
document.getElementById('summary-total').textContent = formatarNumero(total);
document.getElementById('summary-available').textContent = formatarNumero(disp);
renderizarSaldos(saldos);
renderizarLotes(dadosAtuais.dados.lotes || []);
renderizarEmpenhos(dadosAtuais.dados.empenhos || []);
renderizarMovimentacoes(dadosAtuais.dados.movimentacoes || []);
}
// Renderizacao das tabelas
function renderizarSaldos(saldos) { /* ... */ }
function renderizarLotes(lotes) { /* ... */ }
function renderizarEmpenhos(empenhos) { /* ... */ }
function renderizarMovimentacoes(mov) { /* ... */ }
// MATR260 - Relatorio de Saldo Analitico
async function abrirMatr260() {
const response = await fetch(
`${API_BASE}/api/matr260?armazem=${armazemSelecionado}`
);
const data = await response.json();
// Exibe modal com tabela analitica
}
// Exportar Excel
function exportarExcel() {
window.location.href =
`${API_BASE}/api/matr260/excel?armazem=${armazemSelecionado}`;
}
// Inicializacao
document.getElementById('search-form').addEventListener('submit', async (e) => {
e.preventDefault();
const input = document.getElementById('codigo');
await buscarProduto(input.value);
});
/* Variaveis e Design System */
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--success: #16a34a;
--warning: #ca8a04;
--danger: #dc2626;
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--text-primary: #0f172a;
--text-secondary: #64748b;
--border: #e2e8f0;
--radius: 8px;
--font-sans: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
/* Layout */
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex;
align-items: center;
gap: 24px;
position: sticky;
top: 0;
z-index: 100;
}
/* Search Form */
.search-form {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.warehouse-filter {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.filter-btn {
padding: 4px 12px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: transparent;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.15s;
}
.filter-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* Cards de Sumario */
.stock-summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 20px 24px;
}
.summary-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
text-align: center;
}
.summary-card strong {
font-size: 1.5rem;
font-family: var(--font-mono);
color: var(--primary);
display: block;
margin-top: 4px;
}
/* Tabs */
.tabs {
display: flex;
gap: 2px;
padding: 0 24px;
border-bottom: 1px solid var(--border);
}
.tab {
padding: 10px 20px;
border: none;
background: transparent;
cursor: pointer;
font-size: 0.85rem;
color: var(--text-secondary);
border-bottom: 2px solid transparent;
}
.tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
/* Tabelas */
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: var(--bg-tertiary);
padding: 10px 12px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.data-table td {
padding: 10px 12px;
font-size: 0.85rem;
border-bottom: 1px solid var(--border);
}