Spaces:
Runtime error
Runtime error
File size: 22,057 Bytes
548d646 560402b 8ea69c3 548d646 76b5a83 73c59ce 548d646 06e0760 548d646 ae1620c 969aa43 548d646 00ba893 560402b 00ba893 560402b 548d646 73c59ce 247fff4 73c59ce 407239a 548d646 00ba893 548d646 73c59ce c555acd a074f68 548d646 73c59ce a074f68 73c59ce a074f68 969aa43 e85f113 00ba893 e85f113 9d7e91e e85f113 9d7e91e e85f113 9d7e91e e85f113 9d7e91e e85f113 81f3110 9d7e91e 06e0760 9d7e91e 06e0760 9d7e91e ae1620c 548d646 ae1620c 73c59ce a074f68 e85f113 a074f68 00ba893 9d7e91e e85f113 9d7e91e e85f113 9d7e91e 00ba893 e85f113 06e0760 9d7e91e e85f113 00ba893 e85f113 9d7e91e e85f113 9d7e91e e85f113 9d7e91e e85f113 9d7e91e 548d646 9d7e91e e85f113 9d7e91e e85f113 548d646 a074f68 e85f113 969aa43 a074f68 e85f113 a074f68 e85f113 548d646 a074f68 c555acd 969aa43 a074f68 8ea69c3 a074f68 969aa43 8ea69c3 a074f68 c555acd 73c59ce e85f113 a074f68 969aa43 a074f68 c555acd 9d7e91e 548d646 c555acd 548d646 06e0760 548d646 a074f68 ae1620c a074f68 548d646 a074f68 548d646 a074f68 548d646 ae1620c 06e0760 548d646 407239a a074f68 8ea69c3 a074f68 73c59ce 548d646 ae1620c 8ea69c3 a074f68 407239a 969aa43 407239a e85f113 a074f68 548d646 a074f68 548d646 407239a 9d7e91e 407239a 9d7e91e 81f3110 06e0760 a074f68 06e0760 548d646 a074f68 548d646 a074f68 06e0760 a074f68 06e0760 a074f68 06e0760 548d646 a074f68 bc0c97c 247fff4 a074f68 548d646 c555acd 548d646 e85f113 c555acd 00ba893 c555acd 06e0760 c555acd 06e0760 c555acd 06e0760 bc0c97c 06e0760 c555acd 06e0760 548d646 c555acd e85f113 c555acd e85f113 06e0760 548d646 73c59ce c555acd 73c59ce c555acd 73c59ce c555acd 73c59ce e85f113 9d7e91e 06e0760 9d7e91e 06e0760 9d7e91e 06e0760 9d7e91e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | 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") |