CalculadoraITLS / app.py
GabrielGD's picture
Update app.py
06e0760 verified
Raw
History Blame Contribute Delete
22.1 kB
import streamlit as st
import json
import math
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import io
import csv
import os
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as RLImage, Table, TableStyle, PageBreak
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
# ==========================================
# CONFIGURAÇÃO GERAL
# ==========================================
st.set_page_config(page_title="SGP - Gestão de Embalagem v21.0", layout="wide", page_icon="🏭")
FITAS = [
{"nome": "Fita 1.20", "tamanho": 1.20}, {"nome": "Fita 1.40", "tamanho": 1.40},
{"nome": "Fita 1.50", "tamanho": 1.50}, {"nome": "Fita 1.60", "tamanho": 1.60},
{"nome": "Fita 1.70", "tamanho": 1.70}, {"nome": "Fita 1.80", "tamanho": 1.80},
{"nome": "Fita 1.85", "tamanho": 1.85}, {"nome": "Fita 1.90", "tamanho": 1.90},
{"nome": "Fita 2.50", "tamanho": 2.50}, {"nome": "Fita 3.00", "tamanho": 3.00},
{"nome": "Fita 3.50", "tamanho": 3.50}, {"nome": "Fita 4.00", "tamanho": 4.00}
]
MADEIRAS_PADRAO = [370, 400, 500]
DB_FILE = 'catalogo.json'
DENSIDADE_ACO = 7.85e-6
@st.cache_data
def load_db():
if not os.path.exists(DB_FILE): return {"IT13": {}, "IT14": {}}
try:
with open(DB_FILE, 'r') as f: return json.load(f)
except: return {"IT13": {}, "IT14": {}}
def save_db(new_db):
with open(DB_FILE, 'w') as f: json.dump(new_db, f, indent=4)
st.cache_data.clear()
DB = load_db()
# ==========================================
# LÓGICA DE CÁLCULO
# ==========================================
class PackageCalculator:
def _parse_layout(self, layout_str):
try: return [int(x) for x in layout_str.split('-') if x.strip().isdigit()]
except: return []
def _escolher_fita(self, perimetro_m):
p_margem = perimetro_m * 1.10
for f in FITAS:
if f["tamanho"] >= p_margem: return f["nome"], f["tamanho"]
return "Sob Medida", round(p_margem, 2)
def _escolher_madeira(self, w):
for m in MADEIRAS_PADRAO:
if m >= w: return m
return MADEIRAS_PADRAO[-1]
def _buscar_no_banco(self, tipo, l, a, e):
target_db = DB["IT13"] if tipo == "Redondo" else DB["IT14"]
melhor_vizinho = None
menor_distancia = float('inf')
for key, item in target_db.items():
try:
parts = [float(x) for x in key.split('_')]
if tipo == "Redondo":
db_l, db_e = parts[0], parts[1]
if math.isclose(l, db_l, abs_tol=0.1) and math.isclose(e, db_e, abs_tol=0.05): return item, None
if math.isclose(l, db_l, abs_tol=0.1):
dist = abs(e - db_e)
if dist < menor_distancia: menor_distancia, melhor_vizinho = dist, item
else:
db_l, db_a, db_e = parts[0], parts[1], parts[2]
match_n = math.isclose(l, db_l, abs_tol=0.1) and math.isclose(a, db_a, abs_tol=0.1)
match_i = math.isclose(l, db_a, abs_tol=0.1) and math.isclose(a, db_l, abs_tol=0.1)
if (match_n or match_i) and math.isclose(e, db_e, abs_tol=0.05): return item, None
if (match_n or match_i):
dist = abs(e - db_e)
if dist < menor_distancia: menor_distancia, melhor_vizinho = dist, item
except: continue
return None, melhor_vizinho
def simulate_wood_scenarios(self, l, a, e, c, tipo, peso_unit):
scenarios = {}
for madeira_size in MADEIRAS_PADRAO:
if tipo == "Redondo": base = int(madeira_size / l)
else: base = int(madeira_size / l)
if base < 1:
scenarios[madeira_size] = None
continue
qtd_alvo = int(1000 / peso_unit) if peso_unit > 0 else 1
if tipo == "Redondo":
layout = []
curr = 0
while curr < qtd_alvo:
take = base if len(layout)%2==0 else base-1
if (qtd_alvo - curr) < take: take = qtd_alvo - curr
layout.append(take); curr += take
pkg = self._build_pkg(l, a, e, c, tipo, 0, curr, curr*peso_unit, f"Simulação {madeira_size}", True, layout, 0, 0, False, True, 6.60)
pkg['madeira_real'] = madeira_size
else:
ga = math.ceil(qtd_alvo / base)
num = base * ga
pkg = self._build_pkg(l, a, e, c, tipo, 0, num, num*peso_unit, f"Simulação {madeira_size}", False, None, base, ga, False, True, 6.60)
pkg['madeira_real'] = madeira_size
scenarios[madeira_size] = pkg
return scenarios
def calculate(self, modo, meta, l, a, e, c, tipo, acabamento, aba=0):
l, a, e = float(l), float(a), float(e)
is_galv = (acabamento == "Galvanizado (GI/GA)")
wood_h = 6.60 if is_galv else 0.0
item_exato, item_vizinho = self._buscar_no_banco(tipo, l, a, e)
if tipo == "Redondo": area = math.pi * ((l/2)**2 - (l/2-e)**2)
elif tipo == "Enrijecido": area = (l + 2*a + 2*aba - 4*e) * e
else: area = (l + 2*a - 2*e) * e
peso_unit = area * c * DENSIDADE_ACO
# --- CENÁRIO 1: NORMA ---
pkg_norma = None
item_ref = item_exato or item_vizinho
if item_ref:
origem_ref = "Catálogo (Exato)" if item_exato else "Catálogo (Vizinho)"
num_ref = item_ref['qtde']
if item_ref.get('sextavado'):
layout_n = self._parse_layout(item_ref['padrao'])
pkg_norma = self._build_pkg(l, a, e, c, tipo, aba, num_ref, num_ref*peso_unit, origem_ref, True, layout_n, 0, 0, False, is_galv, wood_h)
elif tipo == "Redondo":
gl, ga = item_ref.get('ml', 0), item_ref.get('ma', 0)
pkg_norma = self._build_pkg(l, a, e, c, tipo, aba, num_ref, num_ref*peso_unit, origem_ref, False, None, gl, ga, False, is_galv, wood_h)
else:
gl, ga = item_ref.get('ml', 0), item_ref.get('ma', 0)
pkg_norma = self._build_pkg(l, a, e, c, tipo, aba, num_ref, num_ref*peso_unit, origem_ref, False, None, gl, ga, False, is_galv, wood_h)
# --- CENÁRIO 2: IDEAL ---
base_ideal = 0
override_base = False
target_pecas = pkg_norma['num'] if pkg_norma else int(1000/peso_unit)
if target_pecas == 0: target_pecas = 1
if is_galv and tipo != "Redondo":
if e <= 1.25: base_ideal, override_base = 10, True
elif 1.55 <= e <= 1.95: base_ideal, override_base = 8, True
if is_galv and not override_base:
base_ideal = int(500 / l)
elif not override_base:
if pkg_norma and pkg_norma['gl'] > 0: base_ideal = pkg_norma['gl']
else: base_ideal = int(math.sqrt(target_pecas))
if base_ideal < 1: base_ideal = 1
if tipo == "Redondo":
layout_ideal, curr = [], 0
while curr < target_pecas:
take = base_ideal if len(layout_ideal)%2==0 else base_ideal-1
if (target_pecas - curr) < take: take = target_pecas - curr
layout_ideal.append(take); curr += take
pkg_ideal = self._build_pkg(l, a, e, c, tipo, aba, curr, curr*peso_unit, "Ideal (Físico)", True, layout_ideal, 0, 0, False, is_galv, wood_h)
else:
gl_ideal = base_ideal
ga_ideal = math.ceil(target_pecas / gl_ideal)
num_ideal = gl_ideal * ga_ideal
pkg_ideal = self._build_pkg(l, a, e, c, tipo, aba, num_ideal, num_ideal*peso_unit, "Ideal (Físico)", False, None, gl_ideal, ga_ideal, False, is_galv, wood_h)
# --- SIMULAÇÕES EXTRAS (Se Galv) ---
simulations = {}
if is_galv:
simulations = self.simulate_wood_scenarios(l, a, e, c, tipo, peso_unit)
# Totais
main_ref = pkg_ideal
if modo == "Toneladas": total_pcs = int((meta*1000)/peso_unit) if peso_unit>0 else 0
else: total_pcs = int(meta)
qtd_pcts = total_pcs // main_ref['num'] if main_ref['num'] > 0 else 0
sobra = total_pcs % main_ref['num'] if main_ref['num'] > 0 else 0
sobra_pkg = None
if sobra > 0:
if pkg_ideal['sext']:
rem, s_lay = sobra, []
b = pkg_ideal['layout'][0] if pkg_ideal['layout'] else 5
while rem > 0:
tk = min(rem, b if len(s_lay)%2==0 else b-1)
s_lay.append(tk); rem -= tk
sobra_pkg = self._build_pkg(l, a, e, c, tipo, aba, sobra, sobra*peso_unit, "Sobra", True, s_lay, 0, 0, True, is_galv, wood_h)
else:
s_ga = math.ceil(sobra/pkg_ideal['gl'])
sobra_pkg = self._build_pkg(l, a, e, c, tipo, aba, sobra, sobra*peso_unit, "Sobra", False, None, pkg_ideal['gl'], s_ga, True, is_galv, wood_h)
return {
"norma": pkg_norma, "ideal": pkg_ideal, "sobra": sobra_pkg,
"simulations": simulations,
"total_pcts": qtd_pcts, "peso_total": (qtd_pcts * main_ref['peso']) + (sobra_pkg['peso'] if sobra_pkg else 0)
}
def _build_pkg(self, l, a, e, c, tipo, aba, num, peso, orig, sext, layout, gl, ga, is_rem, is_galv, wood_h):
info = {"l":l, "a":a, "e":e, "c":c, "tipo":tipo, "aba":aba, "num":num, "peso":peso, "origem":orig, "sext":sext, "layout":layout, "gl":gl, "ga":ga, "is_rem":is_rem, "is_galv":is_galv, "wood_h":wood_h, "wood_w": 48.20}
if sext:
r = layout or []
mc = max(r) if r else 1
info["larg_total"] = mc * l
info["alt_total"] = l + (len(r)-1)*l*math.sin(math.radians(60))
if is_galv:
cm = max(0, len(r)-1)
info["alt_total"] += cm * wood_h
info["qtd_madeiras"] = cm * 5
else: info["qtd_madeiras"] = 0
info["layout_str"] = "-".join(map(str, r)) if r else ""
else:
info["larg_total"] = gl*l + (aba if tipo=="Enrijecido" else 0)
h_base = (ga * l) if tipo=="Redondo" else (ga * a)
if is_galv:
cm = max(0, ga-1)
info["alt_total"] = h_base + cm*wood_h
info["qtd_madeiras"] = cm * 5
else:
info["alt_total"] = h_base
info["qtd_madeiras"] = 0
info["layout_str"] = f"{gl} x {ga}"
info["madeira_real"] = self._escolher_madeira(info["larg_total"]) if is_galv else 0
pt = (info["larg_total"] + info["alt_total"])*2/1000
info["fita_nome"], info["fita_tam"] = self._escolher_fita(pt)
info["fita_meio_nome"], info["fita_meio_tam"] = "N/A", 0
if not is_galv:
pm = (info["larg_total"] + info["alt_total"]/2)*2/1000
info["fita_meio_nome"], info["fita_meio_tam"] = self._escolher_fita(pm)
info["desc_fitas"] = "4 Fitas + 1 Meio"
else: info["desc_fitas"] = "5 Fitas Totais"
return info
# ==========================================
# VISUALIZAÇÃO
# ==========================================
def plot_cross_section(info):
fig, ax = plt.subplots(figsize=(6, 5))
l, a, e = info['l'], info['a'], info['e']
wood_h = info['wood_h']
is_galv = info['is_galv']
wood_len = info.get("madeira_real", 0)
total_w = info['larg_total']
fc = 'lightblue' if not info['is_rem'] else 'lightcoral'
ec = 'black'
if info['sext']:
rows = info['layout']
rad = l/2; dy = l*0.866
for i, c in enumerate(rows):
x0 = (total_w - c*l)/2
y = rad + i*dy + i*wood_h
if is_galv and i>0:
wx = (total_w - wood_len)/2
ax.add_patch(patches.Rectangle((wx, y-rad-wood_h), wood_len, wood_h, fc='peru', ec='brown'))
for k in range(c):
cx = x0 + k*l + rad
ax.add_patch(patches.Circle((cx, y), rad, fc=fc, ec=ec))
ax.add_patch(patches.Circle((cx, y), rad-e, fc='white', ec=ec, lw=0.5))
else:
gl, ga = info['gl'], info['ga']
drawn = 0
rad = l/2; dy = l*0.866
for i in range(ga):
if info['tipo']=="Redondo": y = i*(l+wood_h); cy = y + l/2
else: y = i*(a+wood_h); tube_y = y
if is_galv and i>0:
wy = y-wood_h
wx = (total_w - wood_len)/2
ax.add_patch(patches.Rectangle((wx, wy), wood_len, wood_h, fc='peru', ec='brown'))
for j in range(gl):
if drawn >= info['num']: break
x = j*l
if info['tipo']=="Redondo":
cx = x + rad
ax.add_patch(patches.Circle((cx, cy), rad, fc=fc, ec=ec))
ax.add_patch(patches.Circle((cx, cy), rad-e, fc='white', ec=ec, lw=0.5))
else:
ax.add_patch(patches.Rectangle((x, tube_y), l, a, fc=fc, ec=ec))
ax.add_patch(patches.Rectangle((x+e, tube_y+e), l-2*e, a-2*e, fc='white', ec=ec, lw=0.5))
drawn+=1
margin = max(total_w, info['alt_total']) * 0.15
ax.add_patch(patches.Rectangle((-2, -2), total_w+4, info['alt_total']+4, fill=False, ec='red', ls='--', lw=1))
if not info['sext']:
ax.text(total_w/2, info['alt_total']+margin*0.2, f"BASE: {info['gl']}", ha='center', color='blue', weight='bold', fontsize=10)
ax.text(-margin*0.2, info['alt_total']/2, f"ALT: {info['ga']}", ha='right', va='center', color='blue', weight='bold', rotation=90, fontsize=10)
if is_galv and wood_len < total_w:
ax.text(total_w/2, info['alt_total']/2, "MADEIRA CURTA!", ha='center', color='red', weight='bold', fontsize=14, rotation=45)
ax.text(total_w/2, -margin/2, f"L: {total_w:.1f}mm", ha='center')
ax.text(-margin/2, info['alt_total']/2, f"A: {info['alt_total']:.1f}mm", ha='center', rotation=90)
if is_galv: ax.text(total_w/2, -margin, f"Madeira: {wood_len}mm", ha='center', color='brown', weight='bold')
ax.set_xlim(-margin, total_w+margin); ax.set_ylim(-margin, info['alt_total']+margin)
ax.set_aspect('equal'); ax.axis('off')
plt.title(f"{info['origem']} | {info['num']} Pçs", fontsize=10)
return fig
def plot_side_view(info):
fig, ax = plt.subplots(figsize=(8, 2))
c, h = info['c'], info['alt_total']
ax.add_patch(patches.Rectangle((0,0), c, h, fc='lightgray', ec='black'))
if info['is_galv']:
lays = info['ga'] if not info['sext'] else len(info['layout'])
for i in range(1, lays):
if info['sext']: y = i*(info['l']*0.866 + info['wood_h']) - info['wood_h']
else:
y = i*(info['l']+info['wood_h']) - info['wood_h'] if info['tipo']=="Redondo" else i*(info['a']+info['wood_h']) - info['wood_h']
for p in [0.1, 0.3, 0.5, 0.7, 0.9]:
ax.add_patch(patches.Rectangle((c*p-24, y), 48, info['wood_h'], fc='peru'))
for idx, p in enumerate([0.1, 0.3, 0.5, 0.7, 0.9]):
col = 'orange' if (not info['is_galv'] and idx==2) else 'purple'
ls = '--' if col=='orange' else '-'
to_h = h/2 if col=='orange' else h
ax.plot([c*p, c*p], [0, to_h], color=col, lw=2, ls=ls)
ax.set_xlim(-c*0.05, c*1.05); ax.set_ylim(0, h*1.2); ax.axis('off')
plt.title("Vista Lateral", fontsize=8)
return fig
def fig_to_img(fig, w=400, h=300):
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0); plt.close(fig)
return RLImage(buf, width=w, height=h, kind='proportional')
def generate_pdf(res):
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=letter)
styles = getSampleStyleSheet()
story = []
m = res['ideal']
story.append(Paragraph(f"Ficha Técnica: {m['tipo']} {m['l']}x{m['a']} #{m['e']}", styles['Title']))
story.append(Spacer(1, 12))
acab = "Galvanizado (Com Madeira)" if m['is_galv'] else "Preto (Normal)"
fita = f"4x {m['fita_nome']} ({m['fita_tam']}m)"
if m['fita_meio_tam'] > 0: fita += f" + 1x {m['fita_meio_nome']} ({m['fita_meio_tam']}m)"
else: fita = f"5x {m['fita_nome']} ({m['fita_tam']}m)"
wood = f"{m['qtd_madeiras']} pçs (Ref: {m.get('madeira_real', 'N/A')}mm)" if m['is_galv'] else "N/A"
data = [
["Item", f"{m['l']}x{m['a']} #{m['e']}mm"],
["Acabamento", acab],
["Pacotes", f"{res['total_pcts']} (Padrão) + {1 if res['sobra'] else 0} (Sobra)"],
["Peso Padrão", f"{m['peso']:.2f} kg"],
["Cinta", fita],
["Madeiras", wood]
]
t = Table(data, colWidths=[150, 300])
t.setStyle(TableStyle([('GRID', (0,0), (-1,-1), 1, colors.black), ('BACKGROUND', (0,0), (0,-1), colors.lightgrey)]))
story.append(t); story.append(Spacer(1, 20))
story.append(Paragraph("Visualização Pacote Ideal (Produção)", styles['Heading2']))
story.append(fig_to_img(plot_cross_section(m)))
story.append(fig_to_img(plot_side_view(m), h=150))
# ADICIONADO: SIMULAÇÃO DE MADEIRAS NO PDF
if m['is_galv'] and res.get('simulations'):
story.append(PageBreak())
story.append(Paragraph("Simulação de Cenários de Madeira", styles['Heading2']))
sim_data = []
headers = []
imgs = []
for mad_size, scen in res['simulations'].items():
headers.append(f"Madeira {mad_size}mm")
if scen:
img = fig_to_img(plot_cross_section(scen), w=180, h=150)
imgs.append(img)
else:
imgs.append(Paragraph("Não Compatível", styles['Normal']))
sim_data = [headers, imgs]
t_sim = Table(sim_data, colWidths=[190, 190, 190])
t_sim.setStyle(TableStyle([('GRID', (0,0), (-1,-1), 1, colors.black), ('ALIGN', (0,0), (-1,-1), 'CENTER')]))
story.append(t_sim)
if res['norma']:
story.append(PageBreak())
story.append(Paragraph("Comparativo: O que diz a Norma (Tabela)", styles['Heading2']))
story.append(fig_to_img(plot_cross_section(res['norma'])))
if res['sobra']:
story.append(PageBreak())
story.append(Paragraph(f"Pacote de Sobra ({res['sobra']['num']} pçs)", styles['Heading2']))
story.append(fig_to_img(plot_cross_section(res['sobra'])))
doc.build(story)
buffer.seek(0)
return buffer
def generate_csv(res):
output = io.StringIO()
writer = csv.writer(output)
m = res['ideal']
writer.writerow(["Tipo", "L", "A", "E", "Acab", "Qtd Pcts", "Pecas/Pct", "Peso", "Fita", "Madeira"])
acab = "GI/GA" if m['is_galv'] else "Normal"
writer.writerow([m['tipo'], m['l'], m['a'], m['e'], acab, res['total_pcts'], m['num'], m['peso'], m['fita_nome'], m.get('madeira_real', 0)])
return output.getvalue()
# ==========================================
# INTERFACE
# ==========================================
st.title("🏭 SGP - Gestão de Embalagem v21.0")
with st.sidebar:
st.header("Parâmetros")
tipo = st.selectbox("Perfil", ["Quadrado/Retangular", "Redondo", "U/Enrijecido"])
acabamento = st.radio("Acabamento", ["Preto (Normal)", "Galvanizado (GI/GA)"])
c1, c2 = st.columns(2)
l = c1.number_input("L/Diam", value=50.0)
a = c2.number_input("Altura", value=50.0, disabled=(tipo=="Redondo"))
c3, c4 = st.columns(2)
e = c3.number_input("Espessura", value=2.00)
c = c4.number_input("Comp", value=6000)
aba = st.number_input("Aba (U)", value=0.0, disabled=(tipo!="U/Enrijecido"))
meta = st.number_input("Meta", value=1000)
modo = st.selectbox("Unidade", ["Peças", "Toneladas"])
btn = st.button("CALCULAR", type="primary")
if btn:
calc = PackageCalculator()
t_map = "Enrijecido" if tipo == "U/Enrijecido" else tipo
if tipo == "Quadrado/Retangular": t_map = "Quadrado/Retangular"
res = calc.calculate(modo, meta, l, a, e, c, t_map, acabamento, aba)
if res['norma']:
st.info("ℹ️ Encontrado na Norma. A seguir, o cálculo Ideal (Físico).")
# 2. Painel Principal (Recomendado)
st.subheader("✅ Configuração Recomendada")
m = res['ideal']
k1, k2, k3, k4 = st.columns(4)
k1.metric("Pacotes", res['total_pcts'])
k2.metric("Pçs/Pct", m['num'])
k3.metric("Peso", f"{m['peso']:.0f}kg")
k4.metric("Madeira", f"{m.get('madeira_real', 0)}mm" if m['is_galv'] else "N/A")
c_main, c_side = st.columns([1,1])
with c_main: st.pyplot(plot_cross_section(m))
with c_side:
st.pyplot(plot_side_view(m))
if res['sobra']: st.warning(f"Sobra: {res['sobra']['num']} pçs"); st.pyplot(plot_cross_section(res['sobra']))
# 3. SIMULADOR DE MADEIRAS (Mostra no site e agora no PDF)
if m['is_galv']:
st.markdown("---")
st.subheader("🪵 Simulador por Tamanho de Madeira")
cols = st.columns(3)
sims = res['simulations']
for i, mad_size in enumerate(MADEIRAS_PADRAO):
with cols[i]:
st.markdown(f"### Madeira {mad_size}mm")
scenario = sims.get(mad_size)
if scenario:
st.caption(f"{scenario['num']} Pçs | {scenario['peso']:.0f}kg | {scenario['gl']}x{scenario['ga']}")
st.pyplot(plot_cross_section(scenario))
else:
st.error("❌ Não cabe")
# 4. Downloads
st.markdown("---")
c1, c2 = st.columns(2)
c1.download_button("📄 PDF Relatório Completo", generate_pdf(res), "relatorio.pdf", "application/pdf")
c2.download_button("📊 CSV Dados", generate_csv(res), "dados.csv", "text/csv")