Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,7 +14,7 @@ from reportlab.lib.pagesizes import letter
|
|
| 14 |
# ==========================================
|
| 15 |
# CONFIGURAÇÃO GERAL
|
| 16 |
# ==========================================
|
| 17 |
-
st.set_page_config(page_title="SGP - Gestão de Embalagem
|
| 18 |
|
| 19 |
FITAS = [
|
| 20 |
{"nome": "Fita 1.20", "tamanho": 1.20}, {"nome": "Fita 1.40", "tamanho": 1.40},
|
|
@@ -63,10 +63,6 @@ class PackageCalculator:
|
|
| 63 |
return MADEIRAS_PADRAO[-1]
|
| 64 |
|
| 65 |
def _buscar_no_banco(self, tipo, l, a, e):
|
| 66 |
-
"""
|
| 67 |
-
Busca robusta numérica (ignora formatação de string "15" vs "15.00")
|
| 68 |
-
Retorna: (item_exato, item_vizinho)
|
| 69 |
-
"""
|
| 70 |
target_db = DB["IT13"] if tipo == "Redondo" else DB["IT14"]
|
| 71 |
melhor_vizinho = None
|
| 72 |
menor_distancia = float('inf')
|
|
@@ -74,148 +70,158 @@ class PackageCalculator:
|
|
| 74 |
for key, item in target_db.items():
|
| 75 |
try:
|
| 76 |
parts = [float(x) for x in key.split('_')]
|
| 77 |
-
|
| 78 |
if tipo == "Redondo":
|
| 79 |
db_l, db_e = parts[0], parts[1]
|
| 80 |
-
|
| 81 |
-
if math.isclose(l, db_l, abs_tol=0.1) and math.isclose(e, db_e, abs_tol=0.05):
|
| 82 |
-
return item, None # Encontrou exato!
|
| 83 |
-
|
| 84 |
-
# Vizinho (Mesmo diametro, espessura diferente)
|
| 85 |
if math.isclose(l, db_l, abs_tol=0.1):
|
| 86 |
dist = abs(e - db_e)
|
| 87 |
-
if dist < menor_distancia:
|
| 88 |
-
menor_distancia = dist
|
| 89 |
-
melhor_vizinho = item
|
| 90 |
-
|
| 91 |
else:
|
| 92 |
-
# Metalon (Tenta LxA e AxL)
|
| 93 |
db_l, db_a, db_e = parts[0], parts[1], parts[2]
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
if (match_normal or match_inv) and math.isclose(e, db_e, abs_tol=0.05):
|
| 99 |
-
return item, None # Encontrou exato!
|
| 100 |
-
|
| 101 |
-
# Vizinho (Mesma geometria, espessura diferente)
|
| 102 |
-
if (match_normal or match_inv):
|
| 103 |
dist = abs(e - db_e)
|
| 104 |
-
if dist < menor_distancia:
|
| 105 |
-
menor_distancia = dist
|
| 106 |
-
melhor_vizinho = item
|
| 107 |
-
|
| 108 |
except: continue
|
| 109 |
-
|
| 110 |
return None, melhor_vizinho
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
def calculate(self, modo, meta, l, a, e, c, tipo, acabamento, aba=0):
|
| 113 |
l, a, e = float(l), float(a), float(e)
|
| 114 |
is_galv = (acabamento == "Galvanizado (GI/GA)")
|
| 115 |
wood_h = 6.60 if is_galv else 0.0
|
| 116 |
|
| 117 |
-
# Busca Inteligente
|
| 118 |
item_exato, item_vizinho = self._buscar_no_banco(tipo, l, a, e)
|
| 119 |
|
| 120 |
-
# Física
|
| 121 |
if tipo == "Redondo": area = math.pi * ((l/2)**2 - (l/2-e)**2)
|
| 122 |
elif tipo == "Enrijecido": area = (l + 2*a + 2*aba - 4*e) * e
|
| 123 |
else: area = (l + 2*a - 2*e) * e
|
| 124 |
peso_unit = area * c * DENSIDADE_ACO
|
| 125 |
-
peso_metro = area * 1000 * DENSIDADE_ACO
|
| 126 |
|
| 127 |
-
# --- CENÁRIO 1: NORMA
|
| 128 |
pkg_norma = None
|
| 129 |
item_ref = item_exato or item_vizinho
|
| 130 |
-
|
| 131 |
if item_ref:
|
| 132 |
-
origem_ref = "Catálogo (Exato)" if item_exato else "Catálogo (Vizinho
|
| 133 |
-
# Se for vizinho, recalculamos a quantidade baseado no peso, mas mantemos o layout
|
| 134 |
-
# Se for exato, usamos a quantidade da norma
|
| 135 |
-
|
| 136 |
num_ref = item_ref['qtde']
|
| 137 |
-
|
| 138 |
if item_ref.get('sextavado'):
|
| 139 |
layout_n = self._parse_layout(item_ref['padrao'])
|
| 140 |
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)
|
|
|
|
|
|
|
|
|
|
| 141 |
else:
|
| 142 |
-
# Grade
|
| 143 |
gl, ga = item_ref.get('ml', 0), item_ref.get('ma', 0)
|
| 144 |
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)
|
| 145 |
|
| 146 |
-
# --- CENÁRIO 2: IDEAL (
|
| 147 |
-
|
|
|
|
| 148 |
|
| 149 |
-
# Base de cálculo: Começa pela norma se tiver, senão calcula
|
| 150 |
target_pecas = pkg_norma['num'] if pkg_norma else int(1000/peso_unit)
|
| 151 |
if target_pecas == 0: target_pecas = 1
|
| 152 |
|
| 153 |
-
# Regras de Madeira GI/GA (Prioridade sobre a norma para o Ideal)
|
| 154 |
-
base_ideal = 0
|
| 155 |
-
override_base = False
|
| 156 |
-
|
| 157 |
if is_galv and tipo != "Redondo":
|
| 158 |
if e <= 1.25: base_ideal, override_base = 10, True
|
| 159 |
elif 1.55 <= e <= 1.95: base_ideal, override_base = 8, True
|
| 160 |
|
| 161 |
-
# Se não tiver regra de espessura, verifica limite fisico 500mm
|
| 162 |
if is_galv and not override_base:
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
if not override_base:
|
| 168 |
-
# Sem restrição, tenta seguir a norma ou quadrado
|
| 169 |
if pkg_norma and pkg_norma['gl'] > 0: base_ideal = pkg_norma['gl']
|
| 170 |
else: base_ideal = int(math.sqrt(target_pecas))
|
| 171 |
|
| 172 |
if base_ideal < 1: base_ideal = 1
|
| 173 |
|
| 174 |
-
# Montagem Ideal
|
| 175 |
if tipo == "Redondo":
|
| 176 |
-
|
| 177 |
-
layout_ideal = []
|
| 178 |
-
curr, cw = 0, base_ideal
|
| 179 |
while curr < target_pecas:
|
| 180 |
take = base_ideal if len(layout_ideal)%2==0 else base_ideal-1
|
| 181 |
if (target_pecas - curr) < take: take = target_pecas - curr
|
| 182 |
layout_ideal.append(take); curr += take
|
| 183 |
-
|
| 184 |
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)
|
| 185 |
else:
|
| 186 |
-
# Grade
|
| 187 |
gl_ideal = base_ideal
|
| 188 |
ga_ideal = math.ceil(target_pecas / gl_ideal)
|
| 189 |
num_ideal = gl_ideal * ga_ideal
|
| 190 |
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)
|
| 191 |
|
| 192 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
if modo == "Toneladas": total_pcs = int((meta*1000)/peso_unit) if peso_unit>0 else 0
|
| 194 |
else: total_pcs = int(meta)
|
| 195 |
|
| 196 |
-
qtd_pcts = total_pcs //
|
| 197 |
-
sobra = total_pcs %
|
| 198 |
|
| 199 |
sobra_pkg = None
|
| 200 |
if sobra > 0:
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
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)
|
| 212 |
|
| 213 |
return {
|
| 214 |
-
"norma": pkg_norma,
|
| 215 |
-
"
|
| 216 |
-
"
|
| 217 |
-
"total_pcts": qtd_pcts,
|
| 218 |
-
"peso_total": (qtd_pcts * pkg_ideal['peso']) + (sobra_pkg['peso'] if sobra_pkg else 0)
|
| 219 |
}
|
| 220 |
|
| 221 |
def _build_pkg(self, l, a, e, c, tipo, aba, num, peso, orig, sext, layout, gl, ga, is_rem, is_galv, wood_h):
|
|
@@ -245,7 +251,6 @@ class PackageCalculator:
|
|
| 245 |
info["layout_str"] = f"{gl} x {ga}"
|
| 246 |
|
| 247 |
info["madeira_real"] = self._escolher_madeira(info["larg_total"]) if is_galv else 0
|
| 248 |
-
|
| 249 |
pt = (info["larg_total"] + info["alt_total"])*2/1000
|
| 250 |
info["fita_nome"], info["fita_tam"] = self._escolher_fita(pt)
|
| 251 |
|
|
@@ -254,9 +259,7 @@ class PackageCalculator:
|
|
| 254 |
pm = (info["larg_total"] + info["alt_total"]/2)*2/1000
|
| 255 |
info["fita_meio_nome"], info["fita_meio_tam"] = self._escolher_fita(pm)
|
| 256 |
info["desc_fitas"] = "4 Fitas + 1 Meio"
|
| 257 |
-
else:
|
| 258 |
-
info["desc_fitas"] = "5 Fitas Totais"
|
| 259 |
-
|
| 260 |
return info
|
| 261 |
|
| 262 |
# ==========================================
|
|
@@ -264,7 +267,7 @@ class PackageCalculator:
|
|
| 264 |
# ==========================================
|
| 265 |
|
| 266 |
def plot_cross_section(info):
|
| 267 |
-
fig, ax = plt.subplots(figsize=(
|
| 268 |
l, a, e = info['l'], info['a'], info['e']
|
| 269 |
wood_h = info['wood_h']
|
| 270 |
is_galv = info['is_galv']
|
|
@@ -290,7 +293,7 @@ def plot_cross_section(info):
|
|
| 290 |
else:
|
| 291 |
gl, ga = info['gl'], info['ga']
|
| 292 |
drawn = 0
|
| 293 |
-
rad = l/2
|
| 294 |
for i in range(ga):
|
| 295 |
if info['tipo']=="Redondo": y = i*(l+wood_h); cy = y + l/2
|
| 296 |
else: y = i*(a+wood_h); tube_y = y
|
|
@@ -316,41 +319,27 @@ def plot_cross_section(info):
|
|
| 316 |
ax.add_patch(patches.Rectangle((-2, -2), total_w+4, info['alt_total']+4, fill=False, ec='red', ls='--', lw=1))
|
| 317 |
|
| 318 |
if not info['sext']:
|
| 319 |
-
ax.text(total_w/2, info['alt_total']+margin*0.2, f"BASE: {info['gl']}", ha='center', color='blue', weight='bold', fontsize=
|
| 320 |
-
ax.text(-margin*0.2, info['alt_total']/2, f"ALT: {info['ga']}", ha='right', va='center', color='blue', weight='bold', rotation=90, fontsize=
|
| 321 |
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
|
| 326 |
ax.set_xlim(-margin, total_w+margin); ax.set_ylim(-margin, info['alt_total']+margin)
|
| 327 |
ax.set_aspect('equal'); ax.axis('off')
|
| 328 |
-
plt.title(f"{info['origem']} | {info['num']} Pçs | {info['peso']:.1f}kg", fontsize=10)
|
| 329 |
return fig
|
| 330 |
|
| 331 |
def plot_side_view(info):
|
| 332 |
fig, ax = plt.subplots(figsize=(8, 2))
|
| 333 |
c, h = info['c'], info['alt_total']
|
| 334 |
ax.add_patch(patches.Rectangle((0,0), c, h, fc='lightgray', ec='black'))
|
| 335 |
-
|
| 336 |
-
if info['is_galv']:
|
| 337 |
-
lays = info['ga'] if not info['sext'] else len(info['layout'])
|
| 338 |
-
for i in range(1, lays):
|
| 339 |
-
if info['sext']: y = i*(info['l']*0.866 + info['wood_h']) - info['wood_h']
|
| 340 |
-
else:
|
| 341 |
-
y = i*(info['l']+info['wood_h']) - info['wood_h'] if info['tipo']=="Redondo" else i*(info['a']+info['wood_h']) - info['wood_h']
|
| 342 |
-
|
| 343 |
-
for p in [0.1, 0.3, 0.5, 0.7, 0.9]:
|
| 344 |
-
ax.add_patch(patches.Rectangle((c*p-24, y), 48, info['wood_h'], fc='peru'))
|
| 345 |
-
|
| 346 |
for idx, p in enumerate([0.1, 0.3, 0.5, 0.7, 0.9]):
|
| 347 |
col = 'orange' if (not info['is_galv'] and idx==2) else 'purple'
|
| 348 |
ls = '--' if col=='orange' else '-'
|
| 349 |
to_h = h/2 if col=='orange' else h
|
| 350 |
ax.plot([c*p, c*p], [0, to_h], color=col, lw=2, ls=ls)
|
| 351 |
-
|
| 352 |
ax.set_xlim(-c*0.05, c*1.05); ax.set_ylim(0, h*1.2); ax.axis('off')
|
| 353 |
-
plt.title("Vista Lateral", fontsize=8)
|
| 354 |
return fig
|
| 355 |
|
| 356 |
def fig_to_img(fig, w=400, h=300):
|
|
@@ -364,50 +353,25 @@ def generate_pdf(res):
|
|
| 364 |
doc = SimpleDocTemplate(buffer, pagesize=letter)
|
| 365 |
styles = getSampleStyleSheet()
|
| 366 |
story = []
|
| 367 |
-
|
| 368 |
-
# Pacote Ideal é o padrão de produção
|
| 369 |
m = res['ideal']
|
| 370 |
-
|
| 371 |
story.append(Paragraph(f"Ficha Técnica: {m['tipo']} {m['l']}x{m['a']} #{m['e']}", styles['Title']))
|
| 372 |
story.append(Spacer(1, 12))
|
| 373 |
-
|
| 374 |
acab = "Galvanizado (Com Madeira)" if m['is_galv'] else "Preto (Normal)"
|
| 375 |
fita = f"4x {m['fita_nome']} ({m['fita_tam']}m)"
|
| 376 |
if m['fita_meio_tam'] > 0: fita += f" + 1x {m['fita_meio_nome']} ({m['fita_meio_tam']}m)"
|
| 377 |
else: fita = f"5x {m['fita_nome']} ({m['fita_tam']}m)"
|
| 378 |
wood = f"{m['qtd_madeiras']} pçs (Ref: {m.get('madeira_real', 'N/A')}mm)" if m['is_galv'] else "N/A"
|
| 379 |
-
|
| 380 |
-
# Dados do Pacote Ideal
|
| 381 |
-
data = [
|
| 382 |
-
["Item", f"{m['l']}x{m['a']} #{m['e']}mm"],
|
| 383 |
-
["Acabamento", acab],
|
| 384 |
-
["Produção", f"{res['total_pcts']} pct + {1 if res['sobra'] else 0} sobra"],
|
| 385 |
-
["Peso Total", f"{res['peso_total']:.2f} kg"],
|
| 386 |
-
["Cintas", fita],
|
| 387 |
-
["Madeiras", wood]
|
| 388 |
-
]
|
| 389 |
t = Table(data, colWidths=[150, 300])
|
| 390 |
t.setStyle(TableStyle([('GRID', (0,0), (-1,-1), 1, colors.black), ('BACKGROUND', (0,0), (0,-1), colors.lightgrey)]))
|
| 391 |
story.append(t); story.append(Spacer(1, 20))
|
| 392 |
-
|
| 393 |
-
story.append(Paragraph("Visualização Pacote Ideal (Produção)", styles['Heading2']))
|
| 394 |
story.append(fig_to_img(plot_cross_section(m)))
|
| 395 |
-
story.append(fig_to_img(plot_side_view(m)
|
| 396 |
-
|
| 397 |
-
# Comparativo com Norma (se existir)
|
| 398 |
-
if res['norma']:
|
| 399 |
-
story.append(PageBreak())
|
| 400 |
-
norma = res['norma']
|
| 401 |
-
story.append(Paragraph(f"Comparativo: Norma Técnica ({norma['origem']})", styles['Heading2']))
|
| 402 |
-
story.append(Paragraph(f"Atenção: A norma indica {norma['num']} peças, mas para segurança e estabilidade recomendamos o layout Ideal.", styles['Normal']))
|
| 403 |
-
story.append(Spacer(1, 10))
|
| 404 |
-
story.append(fig_to_img(plot_cross_section(norma)))
|
| 405 |
-
|
| 406 |
if res['sobra']:
|
| 407 |
story.append(PageBreak())
|
| 408 |
-
story.append(Paragraph(f"Pacote de Sobra ({res['sobra']['num']} pçs)", styles['Heading2']))
|
| 409 |
story.append(fig_to_img(plot_cross_section(res['sobra'])))
|
| 410 |
-
|
| 411 |
doc.build(story)
|
| 412 |
buffer.seek(0)
|
| 413 |
return buffer
|
|
@@ -424,7 +388,7 @@ def generate_csv(res):
|
|
| 424 |
# ==========================================
|
| 425 |
# INTERFACE
|
| 426 |
# ==========================================
|
| 427 |
-
st.title("🏭 SGP - Gestão de Embalagem
|
| 428 |
|
| 429 |
with st.sidebar:
|
| 430 |
st.header("Parâmetros")
|
|
@@ -447,52 +411,47 @@ if btn:
|
|
| 447 |
if tipo == "Quadrado/Retangular": t_map = "Quadrado/Retangular"
|
| 448 |
res = calc.calculate(modo, meta, l, a, e, c, t_map, acabamento, aba)
|
| 449 |
|
|
|
|
| 450 |
if res['norma']:
|
| 451 |
-
st.info("ℹ️
|
| 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 |
-
adm_peso = st.number_input("Peso", value=0.0)
|
| 490 |
-
adm_ml = st.number_input("Colunas", value=0)
|
| 491 |
-
adm_ma = st.number_input("Linhas", value=0)
|
| 492 |
-
if st.button("Salvar"):
|
| 493 |
-
new = {"ml": int(adm_ml), "ma": int(adm_ma), "qtde": int(adm_qtde), "peso": float(adm_peso)}
|
| 494 |
-
key = f"{adm_l:.2f}_{adm_e:.2f}" if adm_tipo == "IT13" else f"{adm_l:.2f}_{adm_a:.2f}_{adm_e:.2f}"
|
| 495 |
-
if adm_tipo == "IT13": new["tipo"] = "Redondo"
|
| 496 |
-
DB[adm_tipo][key] = new
|
| 497 |
-
save_db(DB)
|
| 498 |
-
st.success("Salvo!")
|
|
|
|
| 14 |
# ==========================================
|
| 15 |
# CONFIGURAÇÃO GERAL
|
| 16 |
# ==========================================
|
| 17 |
+
st.set_page_config(page_title="SGP - Gestão de Embalagem v20.0", layout="wide", page_icon="🏭")
|
| 18 |
|
| 19 |
FITAS = [
|
| 20 |
{"nome": "Fita 1.20", "tamanho": 1.20}, {"nome": "Fita 1.40", "tamanho": 1.40},
|
|
|
|
| 63 |
return MADEIRAS_PADRAO[-1]
|
| 64 |
|
| 65 |
def _buscar_no_banco(self, tipo, l, a, e):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
target_db = DB["IT13"] if tipo == "Redondo" else DB["IT14"]
|
| 67 |
melhor_vizinho = None
|
| 68 |
menor_distancia = float('inf')
|
|
|
|
| 70 |
for key, item in target_db.items():
|
| 71 |
try:
|
| 72 |
parts = [float(x) for x in key.split('_')]
|
|
|
|
| 73 |
if tipo == "Redondo":
|
| 74 |
db_l, db_e = parts[0], parts[1]
|
| 75 |
+
if math.isclose(l, db_l, abs_tol=0.1) and math.isclose(e, db_e, abs_tol=0.05): return item, None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
if math.isclose(l, db_l, abs_tol=0.1):
|
| 77 |
dist = abs(e - db_e)
|
| 78 |
+
if dist < menor_distancia: menor_distancia, melhor_vizinho = dist, item
|
|
|
|
|
|
|
|
|
|
| 79 |
else:
|
|
|
|
| 80 |
db_l, db_a, db_e = parts[0], parts[1], parts[2]
|
| 81 |
+
match_n = math.isclose(l, db_l, abs_tol=0.1) and math.isclose(a, db_a, abs_tol=0.1)
|
| 82 |
+
match_i = math.isclose(l, db_a, abs_tol=0.1) and math.isclose(a, db_l, abs_tol=0.1)
|
| 83 |
+
if (match_n or match_i) and math.isclose(e, db_e, abs_tol=0.05): return item, None
|
| 84 |
+
if (match_n or match_i):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
dist = abs(e - db_e)
|
| 86 |
+
if dist < menor_distancia: menor_distancia, melhor_vizinho = dist, item
|
|
|
|
|
|
|
|
|
|
| 87 |
except: continue
|
|
|
|
| 88 |
return None, melhor_vizinho
|
| 89 |
|
| 90 |
+
def simulate_wood_scenarios(self, l, a, e, c, tipo, peso_unit):
|
| 91 |
+
"""Gera 3 cenários forçando cada tamanho de madeira"""
|
| 92 |
+
scenarios = {}
|
| 93 |
+
|
| 94 |
+
for madeira_size in MADEIRAS_PADRAO:
|
| 95 |
+
# 1. Quantas peças cabem na largura?
|
| 96 |
+
if tipo == "Redondo":
|
| 97 |
+
# Ninho: Base = madeira // diametro
|
| 98 |
+
base = int(madeira_size / l)
|
| 99 |
+
else:
|
| 100 |
+
# Grade: Colunas = madeira // largura
|
| 101 |
+
base = int(madeira_size / l)
|
| 102 |
+
|
| 103 |
+
if base < 1:
|
| 104 |
+
scenarios[madeira_size] = None # Não cabe nem uma peça
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
# 2. Calcula altura para ~1000kg
|
| 108 |
+
qtd_alvo = int(1000 / peso_unit) if peso_unit > 0 else 1
|
| 109 |
+
|
| 110 |
+
if tipo == "Redondo":
|
| 111 |
+
# Monta ninho
|
| 112 |
+
layout = []
|
| 113 |
+
curr = 0
|
| 114 |
+
while curr < qtd_alvo:
|
| 115 |
+
take = base if len(layout)%2==0 else base-1
|
| 116 |
+
if (qtd_alvo - curr) < take: take = qtd_alvo - curr
|
| 117 |
+
layout.append(take); curr += take
|
| 118 |
+
|
| 119 |
+
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)
|
| 120 |
+
# Força a madeira escolhida para visualização
|
| 121 |
+
pkg['madeira_real'] = madeira_size
|
| 122 |
+
|
| 123 |
+
else:
|
| 124 |
+
# Monta grade
|
| 125 |
+
ga = math.ceil(qtd_alvo / base)
|
| 126 |
+
num = base * ga
|
| 127 |
+
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)
|
| 128 |
+
pkg['madeira_real'] = madeira_size
|
| 129 |
+
|
| 130 |
+
scenarios[madeira_size] = pkg
|
| 131 |
+
|
| 132 |
+
return scenarios
|
| 133 |
+
|
| 134 |
def calculate(self, modo, meta, l, a, e, c, tipo, acabamento, aba=0):
|
| 135 |
l, a, e = float(l), float(a), float(e)
|
| 136 |
is_galv = (acabamento == "Galvanizado (GI/GA)")
|
| 137 |
wood_h = 6.60 if is_galv else 0.0
|
| 138 |
|
|
|
|
| 139 |
item_exato, item_vizinho = self._buscar_no_banco(tipo, l, a, e)
|
| 140 |
|
|
|
|
| 141 |
if tipo == "Redondo": area = math.pi * ((l/2)**2 - (l/2-e)**2)
|
| 142 |
elif tipo == "Enrijecido": area = (l + 2*a + 2*aba - 4*e) * e
|
| 143 |
else: area = (l + 2*a - 2*e) * e
|
| 144 |
peso_unit = area * c * DENSIDADE_ACO
|
|
|
|
| 145 |
|
| 146 |
+
# --- CENÁRIO 1: NORMA ---
|
| 147 |
pkg_norma = None
|
| 148 |
item_ref = item_exato or item_vizinho
|
|
|
|
| 149 |
if item_ref:
|
| 150 |
+
origem_ref = "Catálogo (Exato)" if item_exato else "Catálogo (Vizinho)"
|
|
|
|
|
|
|
|
|
|
| 151 |
num_ref = item_ref['qtde']
|
|
|
|
| 152 |
if item_ref.get('sextavado'):
|
| 153 |
layout_n = self._parse_layout(item_ref['padrao'])
|
| 154 |
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)
|
| 155 |
+
elif tipo == "Redondo":
|
| 156 |
+
gl, ga = item_ref.get('ml', 0), item_ref.get('ma', 0)
|
| 157 |
+
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)
|
| 158 |
else:
|
|
|
|
| 159 |
gl, ga = item_ref.get('ml', 0), item_ref.get('ma', 0)
|
| 160 |
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)
|
| 161 |
|
| 162 |
+
# --- CENÁRIO 2: IDEAL (Melhor Madeira) ---
|
| 163 |
+
base_ideal = 0
|
| 164 |
+
override_base = False
|
| 165 |
|
|
|
|
| 166 |
target_pecas = pkg_norma['num'] if pkg_norma else int(1000/peso_unit)
|
| 167 |
if target_pecas == 0: target_pecas = 1
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
if is_galv and tipo != "Redondo":
|
| 170 |
if e <= 1.25: base_ideal, override_base = 10, True
|
| 171 |
elif 1.55 <= e <= 1.95: base_ideal, override_base = 8, True
|
| 172 |
|
|
|
|
| 173 |
if is_galv and not override_base:
|
| 174 |
+
# Escolhe a maior base que cabe na maior madeira (500)
|
| 175 |
+
base_ideal = int(500 / l)
|
| 176 |
+
elif not override_base:
|
|
|
|
|
|
|
|
|
|
| 177 |
if pkg_norma and pkg_norma['gl'] > 0: base_ideal = pkg_norma['gl']
|
| 178 |
else: base_ideal = int(math.sqrt(target_pecas))
|
| 179 |
|
| 180 |
if base_ideal < 1: base_ideal = 1
|
| 181 |
|
|
|
|
| 182 |
if tipo == "Redondo":
|
| 183 |
+
layout_ideal, curr = [], 0
|
|
|
|
|
|
|
| 184 |
while curr < target_pecas:
|
| 185 |
take = base_ideal if len(layout_ideal)%2==0 else base_ideal-1
|
| 186 |
if (target_pecas - curr) < take: take = target_pecas - curr
|
| 187 |
layout_ideal.append(take); curr += take
|
|
|
|
| 188 |
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)
|
| 189 |
else:
|
|
|
|
| 190 |
gl_ideal = base_ideal
|
| 191 |
ga_ideal = math.ceil(target_pecas / gl_ideal)
|
| 192 |
num_ideal = gl_ideal * ga_ideal
|
| 193 |
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)
|
| 194 |
|
| 195 |
+
# --- SIMULAÇÕES EXTRAS (Se Galv) ---
|
| 196 |
+
simulations = {}
|
| 197 |
+
if is_galv:
|
| 198 |
+
simulations = self.simulate_wood_scenarios(l, a, e, c, tipo, peso_unit)
|
| 199 |
+
|
| 200 |
+
# Totais
|
| 201 |
+
main_ref = pkg_ideal
|
| 202 |
if modo == "Toneladas": total_pcs = int((meta*1000)/peso_unit) if peso_unit>0 else 0
|
| 203 |
else: total_pcs = int(meta)
|
| 204 |
|
| 205 |
+
qtd_pcts = total_pcs // main_ref['num'] if main_ref['num'] > 0 else 0
|
| 206 |
+
sobra = total_pcs % main_ref['num'] if main_ref['num'] > 0 else 0
|
| 207 |
|
| 208 |
sobra_pkg = None
|
| 209 |
if sobra > 0:
|
| 210 |
+
if pkg_ideal['sext']:
|
| 211 |
+
rem, s_lay = sobra, []
|
| 212 |
+
b = pkg_ideal['layout'][0] if pkg_ideal['layout'] else 5
|
| 213 |
+
while rem > 0:
|
| 214 |
+
tk = min(rem, b if len(s_lay)%2==0 else b-1)
|
| 215 |
+
s_lay.append(tk); rem -= tk
|
| 216 |
+
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)
|
| 217 |
+
else:
|
| 218 |
+
s_ga = math.ceil(sobra/pkg_ideal['gl'])
|
| 219 |
+
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)
|
|
|
|
| 220 |
|
| 221 |
return {
|
| 222 |
+
"norma": pkg_norma, "ideal": pkg_ideal, "sobra": sobra_pkg,
|
| 223 |
+
"simulations": simulations,
|
| 224 |
+
"total_pcts": qtd_pcts, "peso_total": (qtd_pcts * main_ref['peso']) + (sobra_pkg['peso'] if sobra_pkg else 0)
|
|
|
|
|
|
|
| 225 |
}
|
| 226 |
|
| 227 |
def _build_pkg(self, l, a, e, c, tipo, aba, num, peso, orig, sext, layout, gl, ga, is_rem, is_galv, wood_h):
|
|
|
|
| 251 |
info["layout_str"] = f"{gl} x {ga}"
|
| 252 |
|
| 253 |
info["madeira_real"] = self._escolher_madeira(info["larg_total"]) if is_galv else 0
|
|
|
|
| 254 |
pt = (info["larg_total"] + info["alt_total"])*2/1000
|
| 255 |
info["fita_nome"], info["fita_tam"] = self._escolher_fita(pt)
|
| 256 |
|
|
|
|
| 259 |
pm = (info["larg_total"] + info["alt_total"]/2)*2/1000
|
| 260 |
info["fita_meio_nome"], info["fita_meio_tam"] = self._escolher_fita(pm)
|
| 261 |
info["desc_fitas"] = "4 Fitas + 1 Meio"
|
| 262 |
+
else: info["desc_fitas"] = "5 Fitas Totais"
|
|
|
|
|
|
|
| 263 |
return info
|
| 264 |
|
| 265 |
# ==========================================
|
|
|
|
| 267 |
# ==========================================
|
| 268 |
|
| 269 |
def plot_cross_section(info):
|
| 270 |
+
fig, ax = plt.subplots(figsize=(6, 5)) # Reduzi um pouco para caber 3 na tela
|
| 271 |
l, a, e = info['l'], info['a'], info['e']
|
| 272 |
wood_h = info['wood_h']
|
| 273 |
is_galv = info['is_galv']
|
|
|
|
| 293 |
else:
|
| 294 |
gl, ga = info['gl'], info['ga']
|
| 295 |
drawn = 0
|
| 296 |
+
rad = l/2
|
| 297 |
for i in range(ga):
|
| 298 |
if info['tipo']=="Redondo": y = i*(l+wood_h); cy = y + l/2
|
| 299 |
else: y = i*(a+wood_h); tube_y = y
|
|
|
|
| 319 |
ax.add_patch(patches.Rectangle((-2, -2), total_w+4, info['alt_total']+4, fill=False, ec='red', ls='--', lw=1))
|
| 320 |
|
| 321 |
if not info['sext']:
|
| 322 |
+
ax.text(total_w/2, info['alt_total']+margin*0.2, f"BASE: {info['gl']}", ha='center', color='blue', weight='bold', fontsize=10)
|
| 323 |
+
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)
|
| 324 |
|
| 325 |
+
# Se a madeira for curta, alerta
|
| 326 |
+
if is_galv and wood_len < total_w:
|
| 327 |
+
ax.text(total_w/2, info['alt_total']/2, "MADEIRA CURTA!", ha='center', color='red', weight='bold', fontsize=14, rotation=45)
|
| 328 |
|
| 329 |
ax.set_xlim(-margin, total_w+margin); ax.set_ylim(-margin, info['alt_total']+margin)
|
| 330 |
ax.set_aspect('equal'); ax.axis('off')
|
|
|
|
| 331 |
return fig
|
| 332 |
|
| 333 |
def plot_side_view(info):
|
| 334 |
fig, ax = plt.subplots(figsize=(8, 2))
|
| 335 |
c, h = info['c'], info['alt_total']
|
| 336 |
ax.add_patch(patches.Rectangle((0,0), c, h, fc='lightgray', ec='black'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
for idx, p in enumerate([0.1, 0.3, 0.5, 0.7, 0.9]):
|
| 338 |
col = 'orange' if (not info['is_galv'] and idx==2) else 'purple'
|
| 339 |
ls = '--' if col=='orange' else '-'
|
| 340 |
to_h = h/2 if col=='orange' else h
|
| 341 |
ax.plot([c*p, c*p], [0, to_h], color=col, lw=2, ls=ls)
|
|
|
|
| 342 |
ax.set_xlim(-c*0.05, c*1.05); ax.set_ylim(0, h*1.2); ax.axis('off')
|
|
|
|
| 343 |
return fig
|
| 344 |
|
| 345 |
def fig_to_img(fig, w=400, h=300):
|
|
|
|
| 353 |
doc = SimpleDocTemplate(buffer, pagesize=letter)
|
| 354 |
styles = getSampleStyleSheet()
|
| 355 |
story = []
|
|
|
|
|
|
|
| 356 |
m = res['ideal']
|
|
|
|
| 357 |
story.append(Paragraph(f"Ficha Técnica: {m['tipo']} {m['l']}x{m['a']} #{m['e']}", styles['Title']))
|
| 358 |
story.append(Spacer(1, 12))
|
|
|
|
| 359 |
acab = "Galvanizado (Com Madeira)" if m['is_galv'] else "Preto (Normal)"
|
| 360 |
fita = f"4x {m['fita_nome']} ({m['fita_tam']}m)"
|
| 361 |
if m['fita_meio_tam'] > 0: fita += f" + 1x {m['fita_meio_nome']} ({m['fita_meio_tam']}m)"
|
| 362 |
else: fita = f"5x {m['fita_nome']} ({m['fita_tam']}m)"
|
| 363 |
wood = f"{m['qtd_madeiras']} pçs (Ref: {m.get('madeira_real', 'N/A')}mm)" if m['is_galv'] else "N/A"
|
| 364 |
+
data = [["Item", f"{m['l']}x{m['a']} #{m['e']}mm"], ["Acabamento", acab], ["Produção", f"{res['total_pcts']} pct + {1 if res['sobra'] else 0} sobra"], ["Peso Total", f"{res['peso_total']:.2f} kg"], ["Cintas", fita], ["Madeiras", wood]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
t = Table(data, colWidths=[150, 300])
|
| 366 |
t.setStyle(TableStyle([('GRID', (0,0), (-1,-1), 1, colors.black), ('BACKGROUND', (0,0), (0,-1), colors.lightgrey)]))
|
| 367 |
story.append(t); story.append(Spacer(1, 20))
|
| 368 |
+
story.append(Paragraph(f"1. Pacote Padrão ({m['num']} pçs)", styles['Heading2']))
|
|
|
|
| 369 |
story.append(fig_to_img(plot_cross_section(m)))
|
| 370 |
+
story.append(fig_to_img(plot_side_view(m)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
if res['sobra']:
|
| 372 |
story.append(PageBreak())
|
| 373 |
+
story.append(Paragraph(f"2. Pacote de Sobra ({res['sobra']['num']} pçs)", styles['Heading2']))
|
| 374 |
story.append(fig_to_img(plot_cross_section(res['sobra'])))
|
|
|
|
| 375 |
doc.build(story)
|
| 376 |
buffer.seek(0)
|
| 377 |
return buffer
|
|
|
|
| 388 |
# ==========================================
|
| 389 |
# INTERFACE
|
| 390 |
# ==========================================
|
| 391 |
+
st.title("🏭 SGP - Gestão de Embalagem v20.0")
|
| 392 |
|
| 393 |
with st.sidebar:
|
| 394 |
st.header("Parâmetros")
|
|
|
|
| 411 |
if tipo == "Quadrado/Retangular": t_map = "Quadrado/Retangular"
|
| 412 |
res = calc.calculate(modo, meta, l, a, e, c, t_map, acabamento, aba)
|
| 413 |
|
| 414 |
+
# 1. Se tiver Norma, avisa
|
| 415 |
if res['norma']:
|
| 416 |
+
st.info("ℹ️ Encontrado na Norma. A seguir, o cálculo Ideal (Físico).")
|
| 417 |
+
|
| 418 |
+
# 2. Painel Principal (Recomendado)
|
| 419 |
+
st.subheader("✅ Configuração Recomendada")
|
| 420 |
+
m = res['ideal']
|
| 421 |
+
k1, k2, k3, k4 = st.columns(4)
|
| 422 |
+
k1.metric("Pacotes", res['total_pcts'])
|
| 423 |
+
k2.metric("Pçs/Pct", m['num'])
|
| 424 |
+
k3.metric("Peso", f"{m['peso']:.0f}kg")
|
| 425 |
+
k4.metric("Madeira", f"{m.get('madeira_real', 0)}mm" if m['is_galv'] else "N/A")
|
| 426 |
+
|
| 427 |
+
c_main, c_side = st.columns([1,1])
|
| 428 |
+
with c_main: st.pyplot(plot_cross_section(m))
|
| 429 |
+
with c_side:
|
| 430 |
+
st.pyplot(plot_side_view(m))
|
| 431 |
+
if res['sobra']: st.warning(f"Sobra: {res['sobra']['num']} pçs"); st.pyplot(plot_cross_section(res['sobra']))
|
| 432 |
+
|
| 433 |
+
# 3. SIMULADOR DE MADEIRAS (A Cereja do Bolo 🍒)
|
| 434 |
+
if m['is_galv']:
|
| 435 |
+
st.markdown("---")
|
| 436 |
+
st.subheader("🪵 Simulador por Tamanho de Madeira")
|
| 437 |
+
st.write("Compare como o pacote ficaria forçando cada tamanho de madeira disponível:")
|
| 438 |
|
| 439 |
+
cols = st.columns(3)
|
| 440 |
+
sims = res['simulations']
|
| 441 |
+
|
| 442 |
+
for i, mad_size in enumerate(MADEIRAS_PADRAO):
|
| 443 |
+
with cols[i]:
|
| 444 |
+
st.markdown(f"### Madeira {mad_size}mm")
|
| 445 |
+
scenario = sims.get(mad_size)
|
| 446 |
+
|
| 447 |
+
if scenario:
|
| 448 |
+
st.caption(f"{scenario['num']} Pçs | {scenario['peso']:.0f}kg | {scenario['gl']}x{scenario['ga']}")
|
| 449 |
+
st.pyplot(plot_cross_section(scenario))
|
| 450 |
+
else:
|
| 451 |
+
st.error("❌ Não cabe (Peça maior que a madeira)")
|
| 452 |
+
|
| 453 |
+
# 4. Downloads
|
| 454 |
+
st.markdown("---")
|
| 455 |
+
c1, c2 = st.columns(2)
|
| 456 |
+
c1.download_button("📄 PDF Relatório", generate_pdf(res), "relatorio.pdf", "application/pdf")
|
| 457 |
+
c2.download_button("📊 CSV Dados", generate_csv(res), "dados.csv", "text/csv")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|