GabrielGD commited on
Commit
9d7e91e
·
verified ·
1 Parent(s): e85f113

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -182
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 v19.0", layout="wide", page_icon="🏭")
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
- # Match Exato (com pequena tolerância de float)
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
- match_normal = math.isclose(l, db_l, abs_tol=0.1) and math.isclose(a, db_a, abs_tol=0.1)
96
- match_inv = math.isclose(l, db_a, abs_tol=0.1) and math.isclose(a, db_l, abs_tol=0.1)
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 (Referência) ---
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 Próximo)"
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 (Otimizado) ---
147
- # Aqui aplicamos a lógica de madeira e estabilidade
 
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
- if (math.sqrt(target_pecas) * l) > 500:
164
- base_ideal = int(500 / l)
165
- override_base = True
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
- # Ninho sempre
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
- # Totais (Usando Ideal como base de produção)
 
 
 
 
 
 
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 // pkg_ideal['num'] if pkg_ideal['num'] > 0 else 0
197
- sobra = total_pcs % pkg_ideal['num'] if pkg_ideal['num'] > 0 else 0
198
 
199
  sobra_pkg = None
200
  if sobra > 0:
201
- # Gera sobra baseada no layout ideal
202
- if pkg_ideal['sext']:
203
- rem, s_lay = sobra, []
204
- b = pkg_ideal['layout'][0] if pkg_ideal['layout'] else 5
205
- while rem > 0:
206
- tk = min(rem, b if len(s_lay)%2==0 else b-1)
207
- s_lay.append(tk); rem-=tk
208
- 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)
209
- else:
210
- s_ga = math.ceil(sobra/pkg_ideal['gl'])
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
- "ideal": pkg_ideal,
216
- "sobra": sobra_pkg,
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=(8, 6))
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; dy = l*0.866
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=12)
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=12)
321
 
322
- ax.text(total_w/2, -margin/2, f"L: {total_w:.1f}mm", ha='center')
323
- ax.text(-margin/2, info['alt_total']/2, f"A: {info['alt_total']:.1f}mm", ha='center', rotation=90)
324
- if is_galv: ax.text(total_w/2, -margin, f"Madeira: {wood_len}mm", ha='center', color='brown', weight='bold')
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), h=150))
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 v19.0")
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("ℹ️ Item de Norma encontrado! Exibindo versão Ideal e Norma.")
452
- t1, t2, t3 = st.tabs(["✨ Ideal (Físico)", "📋 Norma (Tabela)", "📄 Downloads"])
453
- with t2:
454
- m = res['norma']
455
- st.write(f"**Norma:** {m['num']} peças | {m['peso']:.1f}kg")
456
- c1, c2 = st.columns([1,1])
457
- with c1: st.pyplot(plot_cross_section(m))
458
- with c2: st.pyplot(plot_side_view(m))
459
- else:
460
- t1, t3 = st.tabs(["✨ Ideal (Calculado)", "📄 Downloads"])
461
-
462
- with t1:
463
- m = res['ideal']
464
- k1, k2, k3, k4 = st.columns(4)
465
- k1.metric("Pacotes", res['total_pcts'])
466
- k2.metric("Pçs/Pct", m['num'])
467
- k3.metric("Peso", f"{m['peso']:.0f}kg")
468
- k4.metric("Madeiras", m['qtd_madeiras'])
 
 
 
 
469
 
470
- c1, c2 = st.columns([1,1])
471
- with c1: st.pyplot(plot_cross_section(m))
472
- with c2:
473
- st.pyplot(plot_side_view(m))
474
- if res['sobra']: st.warning(f"Sobra: {res['sobra']['num']} pçs"); st.pyplot(plot_cross_section(res['sobra']))
475
-
476
- with t3:
477
- st.download_button("📄 Baixar PDF (Completo)", generate_pdf(res), "relatorio.pdf", "application/pdf")
478
- st.download_button("📊 Baixar CSV", generate_csv(res), "dados.csv", "text/csv")
479
-
480
- with st.expander("Admin - Catálogo"):
481
- col_a, col_b = st.columns(2)
482
- with col_a:
483
- adm_tipo = st.selectbox("Norma", ["IT14", "IT13"])
484
- adm_l = st.number_input("L", value=0.0)
485
- adm_a = st.number_input("A", value=0.0, disabled=(adm_tipo=="IT13"))
486
- adm_e = st.number_input("E", value=0.0)
487
- with col_b:
488
- adm_qtde = st.number_input("Qtd", value=0)
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")