Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -12,25 +12,13 @@ from reportlab.lib import colors
|
|
| 12 |
from reportlab.lib.pagesizes import letter
|
| 13 |
|
| 14 |
# ==========================================
|
| 15 |
-
#
|
| 16 |
# ==========================================
|
| 17 |
-
st.set_page_config(page_title="SGP - Gestão de Embalagem
|
| 18 |
|
| 19 |
-
# Configurações de Negócio
|
| 20 |
PESO_IDEAL_MIN = 970
|
| 21 |
PESO_IDEAL_MAX = 1080
|
| 22 |
PESO_LIMITE_SEGURANCA = 1200
|
| 23 |
-
DENSIDADE_ACO = 7.85e-6
|
| 24 |
-
|
| 25 |
-
# Tolerâncias e Margens
|
| 26 |
-
TOLERANCIA_DIMENSAO = 0.1 # mm (para match exato)
|
| 27 |
-
TOLERANCIA_BUSCA_GEO = 5.0 # mm (para busca por similaridade)
|
| 28 |
-
MARGEM_FITA = 1.10 # 10% de margem para fita
|
| 29 |
-
|
| 30 |
-
# Dimensões de Insumos
|
| 31 |
-
MADEIRA_H = 6.60
|
| 32 |
-
MADEIRA_W = 48.20
|
| 33 |
-
MADEIRAS_PADRAO = [370, 400, 500]
|
| 34 |
|
| 35 |
FITAS = [
|
| 36 |
{"nome": "Fita 1.20", "tamanho": 1.20}, {"nome": "Fita 1.40", "tamanho": 1.40},
|
|
@@ -41,233 +29,196 @@ FITAS = [
|
|
| 41 |
{"nome": "Fita 3.50", "tamanho": 3.50}, {"nome": "Fita 4.00", "tamanho": 4.00}
|
| 42 |
]
|
| 43 |
|
|
|
|
|
|
|
| 44 |
DB_FILE = 'catalogo.json'
|
| 45 |
|
| 46 |
-
# ==========================================
|
| 47 |
-
# 2. GERENCIAMENTO DE DADOS
|
| 48 |
-
# ==========================================
|
| 49 |
@st.cache_data
|
| 50 |
-
def load_db()
|
| 51 |
-
if not os.path.exists(DB_FILE):
|
| 52 |
-
return {"IT13": {}, "IT14": {}}
|
| 53 |
-
try:
|
| 54 |
-
with open(DB_FILE, 'r') as f:
|
| 55 |
-
return json.load(f)
|
| 56 |
-
except json.JSONDecodeError:
|
| 57 |
-
st.error("Erro: O arquivo catalogo.json está corrompido.")
|
| 58 |
-
return {"IT13": {}, "IT14": {}}
|
| 59 |
-
except Exception as e:
|
| 60 |
-
st.error(f"Erro desconhecido ao carregar DB: {e}")
|
| 61 |
-
return {"IT13": {}, "IT14": {}}
|
| 62 |
-
|
| 63 |
-
def save_db(new_db: dict):
|
| 64 |
try:
|
| 65 |
-
with open(DB_FILE, '
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
| 70 |
|
| 71 |
DB = load_db()
|
| 72 |
|
| 73 |
# ==========================================
|
| 74 |
-
#
|
| 75 |
# ==========================================
|
| 76 |
|
| 77 |
class PackageCalculator:
|
| 78 |
-
def _parse_layout(self, layout_str
|
| 79 |
try:
|
| 80 |
return [int(x) for x in layout_str.split('-') if x.strip().isdigit()]
|
| 81 |
except:
|
| 82 |
return []
|
| 83 |
|
| 84 |
-
def
|
| 85 |
-
"""Valida inputs e retorna lista de erros."""
|
| 86 |
-
erros = []
|
| 87 |
-
if l <= 0 or e <= 0 or c <= 0:
|
| 88 |
-
erros.append("Dimensões devem ser positivas")
|
| 89 |
-
if tipo != "Redondo" and a <= 0:
|
| 90 |
-
erros.append("Altura deve ser positiva para perfis não-redondos")
|
| 91 |
-
if tipo == "Redondo" and e >= l/2:
|
| 92 |
-
erros.append("Espessura não pode ser maior ou igual ao raio")
|
| 93 |
-
return erros
|
| 94 |
-
|
| 95 |
-
def _get_physical_props(self, tipo: str, l: float, a: float, e: float, c: float, aba: float = 0) -> tuple:
|
| 96 |
-
"""Calcula Área da Seção e Peso Unitário de forma centralizada."""
|
| 97 |
-
if tipo == "Redondo":
|
| 98 |
-
area = math.pi * ((l/2)**2 - (l/2-e)**2)
|
| 99 |
-
elif tipo == "Enrijecido":
|
| 100 |
-
area = (l + 2*a + 2*aba - 4*e) * e
|
| 101 |
-
else: # Quadrado/Retangular
|
| 102 |
-
area = (l + 2*a - 2*e) * e
|
| 103 |
-
|
| 104 |
-
peso_unitario = area * c * DENSIDADE_ACO
|
| 105 |
-
return area, peso_unitario
|
| 106 |
-
|
| 107 |
-
def _find_smart_neighbor(self, tipo: str, l: float, a: float, e: float):
|
| 108 |
-
"""Busca inteligente com tolerâncias definidas."""
|
| 109 |
best_item = None
|
| 110 |
-
|
| 111 |
-
min_dist_geo = float('inf')
|
| 112 |
target_db = DB["IT13"] if tipo == "Redondo" else DB["IT14"]
|
| 113 |
|
| 114 |
for key, item in target_db.items():
|
| 115 |
try:
|
| 116 |
parts = [float(x) for x in key.split('_')]
|
| 117 |
if tipo == "Redondo":
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
min_diff_thickness = diff_e
|
| 124 |
-
best_item = item
|
| 125 |
-
# Prioridade 2: Geometria próxima
|
| 126 |
-
elif min_diff_thickness == float('inf'):
|
| 127 |
-
dist = math.sqrt((l - db_l)**2 + (e - db_e)**2)
|
| 128 |
-
if dist < min_dist_geo and dist < TOLERANCIA_BUSCA_GEO:
|
| 129 |
-
min_dist_geo = dist
|
| 130 |
best_item = item
|
| 131 |
else:
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
min_diff_thickness = diff_e
|
| 139 |
-
best_item = item
|
| 140 |
-
elif min_diff_thickness == float('inf'):
|
| 141 |
-
dist = math.sqrt((l - db_l)**2 + (a - db_a)**2)
|
| 142 |
-
if dist < min_dist_geo and dist < (TOLERANCIA_BUSCA_GEO * 2): # Tolerância maior para retangulos
|
| 143 |
-
min_dist_geo = dist
|
| 144 |
best_item = item
|
| 145 |
-
except:
|
| 146 |
-
continue
|
| 147 |
return best_item
|
| 148 |
|
| 149 |
-
def
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
return 0, ""
|
| 156 |
-
|
| 157 |
-
def _resolve_layout(self, item, tipo, peso_unit):
|
| 158 |
-
"""Define a geometria do pacote."""
|
| 159 |
-
layout = None
|
| 160 |
-
is_sext = False
|
| 161 |
-
gl, ga = 0, 0
|
| 162 |
-
num_pecas = 0
|
| 163 |
-
layout_str = ""
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
layout = self._parse_layout(item.get('padrao', ''))
|
| 170 |
-
is_sext, gl, ga = True, 0, len(layout)
|
| 171 |
-
layout_str = item.get('padrao', '')
|
| 172 |
-
|
| 173 |
-
elif tipo == "Redondo":
|
| 174 |
-
# Conversão Grade -> Ninho
|
| 175 |
-
base_width = item.get('ml', 0)
|
| 176 |
-
if base_width == 0: base_width = int(math.sqrt(num_pecas))
|
| 177 |
-
layout = []
|
| 178 |
-
rem = num_pecas
|
| 179 |
-
curr_w = base_width
|
| 180 |
-
while rem > 0:
|
| 181 |
-
take = min(rem, curr_w)
|
| 182 |
-
layout.append(take)
|
| 183 |
-
rem -= take
|
| 184 |
-
curr_w = base_width - 1 if curr_w == base_width else base_width
|
| 185 |
-
is_sext, gl, ga = True, 0, len(layout)
|
| 186 |
-
layout_str = "-".join(map(str, layout))
|
| 187 |
-
|
| 188 |
-
else:
|
| 189 |
-
is_sext, gl, ga = False, item.get('ml', 0), item.get('ma', 0)
|
| 190 |
-
|
| 191 |
-
else:
|
| 192 |
-
# Matemático
|
| 193 |
-
peso_alvo = 1000
|
| 194 |
-
num_pecas = int(peso_alvo / peso_unit) if peso_unit > 0 else 1
|
| 195 |
-
|
| 196 |
-
if tipo == "Redondo":
|
| 197 |
-
side = int(math.sqrt(num_pecas/3))
|
| 198 |
-
base = side + 2
|
| 199 |
-
layout = []
|
| 200 |
-
curr = 0
|
| 201 |
-
for i in range(50):
|
| 202 |
-
cols = base if i%2==0 else base-1
|
| 203 |
-
layout.append(cols); curr+=cols
|
| 204 |
-
if curr>=num_pecas: num_pecas=curr; break
|
| 205 |
-
is_sext, gl, ga = True, 0, len(layout)
|
| 206 |
-
layout_str = "-".join(map(str, layout))
|
| 207 |
-
else:
|
| 208 |
-
is_sext, gl, ga = False, int(math.sqrt(num_pecas)), 0
|
| 209 |
-
if gl > 0: ga = math.ceil(num_pecas/gl)
|
| 210 |
-
else: gl, ga = 1, 1
|
| 211 |
-
|
| 212 |
-
return num_pecas, layout, is_sext, gl, ga, layout_str
|
| 213 |
|
| 214 |
def calculate(self, modo, meta, l, a, e, c, tipo, acabamento, aba=0):
|
| 215 |
-
# 1. Validação
|
| 216 |
-
if erros := self._validar_inputs(l, a, e, c, tipo):
|
| 217 |
-
return {"erro": f"Entrada Inválida: {', '.join(erros)}"}
|
| 218 |
-
|
| 219 |
-
# 2. Normalização e Chaves
|
| 220 |
l, a, e = float(l), float(a), float(e)
|
| 221 |
is_galv = (acabamento == "Galvanizado (GI/GA)")
|
| 222 |
-
wood_h =
|
| 223 |
-
|
|
|
|
| 224 |
key_red = f"{l:.2f}_{e:.2f}"
|
| 225 |
key_met = f"{l:.2f}_{a:.2f}_{e:.2f}"
|
| 226 |
key_met_inv = f"{a:.2f}_{l:.2f}_{e:.2f}"
|
| 227 |
-
|
| 228 |
item = None
|
| 229 |
if tipo == "Redondo":
|
| 230 |
item = DB["IT13"].get(key_red)
|
| 231 |
else:
|
| 232 |
item = DB["IT14"].get(key_met) or DB["IT14"].get(key_met_inv)
|
| 233 |
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
| 240 |
origem = ""
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
if item:
|
| 256 |
origem = "Catálogo (Norma)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
else:
|
| 258 |
-
#
|
| 259 |
-
|
| 260 |
-
|
|
|
|
| 261 |
origem = "Engenharia (Similar)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
else:
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
peso_pct = num_pecas * peso_unit
|
| 269 |
|
| 270 |
-
# 5. Totais
|
| 271 |
if modo == "Toneladas":
|
| 272 |
total_pcs = int((meta * 1000) / peso_unit) if peso_unit > 0 else 0
|
| 273 |
else:
|
|
@@ -276,8 +227,7 @@ class PackageCalculator:
|
|
| 276 |
qtd_pcts = total_pcs // num_pecas if num_pecas > 0 else 0
|
| 277 |
sobra = total_pcs % num_pecas
|
| 278 |
|
| 279 |
-
|
| 280 |
-
main_pkg = self._build_pkg_obj(l, a, e, c, tipo, aba, num_pecas, peso_pct, origem, is_sext, layout, gl, ga, False, is_galv, wood_h, layout_str)
|
| 281 |
|
| 282 |
sobra_pkg = None
|
| 283 |
if sobra > 0:
|
|
@@ -288,17 +238,17 @@ class PackageCalculator:
|
|
| 288 |
cols = base if k%2==0 else base-1
|
| 289 |
take = min(rem, cols); s_layout.append(take); rem-=take
|
| 290 |
if rem<=0: break
|
| 291 |
-
sobra_pkg = self.
|
| 292 |
else:
|
| 293 |
s_ga = math.ceil(sobra / gl) if gl>0 else 1
|
| 294 |
-
sobra_pkg = self.
|
| 295 |
|
| 296 |
return {
|
| 297 |
-
"main":
|
| 298 |
"total_pcts": qtd_pcts, "peso_total": (qtd_pcts * peso_pct) + (sobra_pkg['peso'] if sobra_pkg else 0)
|
| 299 |
}
|
| 300 |
|
| 301 |
-
def
|
| 302 |
info = {
|
| 303 |
"l": l, "a": a, "e": e, "c": c, "tipo": tipo, "aba": aba,
|
| 304 |
"num": num, "peso": peso, "origem": orig,
|
|
@@ -307,66 +257,56 @@ class PackageCalculator:
|
|
| 307 |
"layout_str": layout_str
|
| 308 |
}
|
| 309 |
|
| 310 |
-
|
| 311 |
if sext:
|
| 312 |
rows = layout or []
|
| 313 |
max_c = max(rows) if rows else 1
|
| 314 |
info["larg_total"] = max_c * l
|
| 315 |
h_geo = l + (len(rows) - 1) * l * math.sin(math.radians(60))
|
| 316 |
if is_galv:
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
else:
|
| 321 |
-
|
| 322 |
-
|
| 323 |
else:
|
|
|
|
| 324 |
info["larg_total"] = gl * l
|
| 325 |
if tipo == "Enrijecido": info["larg_total"] += aba
|
| 326 |
|
| 327 |
-
|
| 328 |
-
#
|
| 329 |
-
if tipo =
|
| 330 |
-
h_base = l + (ga - 1) * l * math.sin(math.radians(60))
|
| 331 |
|
| 332 |
if is_galv:
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
else:
|
| 337 |
-
|
| 338 |
-
|
| 339 |
|
| 340 |
-
# Escolha Madeira Real (apenas se galv)
|
| 341 |
if is_galv:
|
| 342 |
-
|
| 343 |
-
if m >= info["larg_total"]:
|
| 344 |
-
info["madeira_real"] = m
|
| 345 |
-
break
|
| 346 |
-
else:
|
| 347 |
-
info["madeira_real"] = MADEIRAS_PADRAO[-1]
|
| 348 |
else:
|
| 349 |
-
info["
|
| 350 |
|
| 351 |
-
|
| 352 |
-
perim = (info["larg_total"] + info["alt_total"]) * 2 / 1000
|
| 353 |
-
for f in FITAS:
|
| 354 |
-
if f["tamanho"] >= perim * MARGEM_FITA:
|
| 355 |
-
info["fita_nome"] = f["nome"]
|
| 356 |
-
info["fita_tam"] = f["tamanho"]
|
| 357 |
-
break
|
| 358 |
-
else:
|
| 359 |
-
info["fita_nome"], info["fita_tam"] = "Sob Medida", perim * MARGEM_FITA
|
| 360 |
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
if not is_galv:
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
info["fita_meio_tam"] = f["tamanho"]
|
| 369 |
-
break
|
| 370 |
info["desc_fitas"] = "4 Fitas Totais + 1 Fita Meio"
|
| 371 |
else:
|
| 372 |
info["desc_fitas"] = "5 Fitas Totais (Pacote Completo)"
|
|
@@ -374,21 +314,22 @@ class PackageCalculator:
|
|
| 374 |
return info
|
| 375 |
|
| 376 |
# ==========================================
|
| 377 |
-
#
|
| 378 |
# ==========================================
|
| 379 |
|
| 380 |
def plot_cross_section(info):
|
| 381 |
fig, ax = plt.subplots(figsize=(10, 8))
|
| 382 |
l, a, e = info['l'], info['a'], info['e']
|
| 383 |
-
wood_h = info['wood_h']
|
| 384 |
is_galv = info['is_galv']
|
| 385 |
-
wood_len = info.get("
|
| 386 |
|
| 387 |
fc = 'lightblue' if not info['is_rem'] else 'lightcoral'
|
| 388 |
ec = 'black'
|
| 389 |
total_w_pkg = info['larg_total']
|
| 390 |
|
| 391 |
if info['sext']:
|
|
|
|
| 392 |
rows = info['layout']
|
| 393 |
radius = l / 2
|
| 394 |
dy = l * 0.866
|
|
@@ -398,48 +339,50 @@ def plot_cross_section(info):
|
|
| 398 |
y_base = radius + i * dy + (i * wood_h)
|
| 399 |
if is_galv and i > 0:
|
| 400 |
wy = y_base - radius - wood_h
|
| 401 |
-
|
| 402 |
-
ax.add_patch(patches.Rectangle((
|
| 403 |
for k in range(count):
|
| 404 |
cx = x0 + k * l + radius
|
| 405 |
ax.add_patch(patches.Circle((cx, y_base), radius, fc=fc, ec=ec))
|
| 406 |
ax.add_patch(patches.Circle((cx, y_base), radius-e, fc='white', ec=ec, lw=0.5))
|
| 407 |
else:
|
|
|
|
| 408 |
gl, ga = info['gl'], info['ga']
|
| 409 |
drawn = 0
|
| 410 |
-
|
| 411 |
-
dy = l * 0.866
|
| 412 |
for i in range(ga):
|
|
|
|
| 413 |
if info['tipo'] == "Redondo":
|
| 414 |
-
y =
|
|
|
|
|
|
|
| 415 |
else:
|
| 416 |
y = i * (a + wood_h)
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
|
| 425 |
for j in range(gl):
|
| 426 |
if drawn >= info['num']: break
|
|
|
|
|
|
|
| 427 |
if info['tipo'] == "Redondo":
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
cx
|
| 431 |
-
cy =
|
| 432 |
-
ax.add_patch(patches.Circle((cx, cy), radius, fc=fc, ec=ec))
|
| 433 |
-
ax.add_patch(patches.Circle((cx, cy), radius-e, fc='white', ec=ec, lw=0.5))
|
| 434 |
else:
|
| 435 |
-
x = j * l
|
| 436 |
if info['tipo'] == "Enrijecido":
|
| 437 |
-
ax.add_patch(patches.Rectangle((x,
|
| 438 |
-
ax.add_patch(patches.Rectangle((x+e,
|
| 439 |
else:
|
| 440 |
-
ax.add_patch(patches.Rectangle((x,
|
| 441 |
inset = min(l, a)*0.1
|
| 442 |
-
ax.add_patch(patches.Rectangle((x+inset,
|
| 443 |
drawn += 1
|
| 444 |
|
| 445 |
L_tot, A_tot = info['larg_total'], info['alt_total']
|
|
@@ -449,13 +392,13 @@ def plot_cross_section(info):
|
|
| 449 |
ax.add_patch(patches.Rectangle((-2, -2), L_tot+4, A_tot+4, fill=False, ec='red', ls='--', lw=1.5, label='Limite'))
|
| 450 |
|
| 451 |
if not info['sext']:
|
| 452 |
-
ax.text(L_tot/2, A_tot+margin*0.1, f"BASE: {info['gl']} PEÇAS", ha='center', va='bottom',
|
| 453 |
-
ax.text(-margin*0.1, A_tot/2, f"ALTURA: {info['ga']} CAMADAS", ha='right', va='center',
|
| 454 |
else:
|
| 455 |
txt = info.get('layout_str') or f"Sextavado ({len(info['layout'])} camadas)"
|
| 456 |
-
ax.text(L_tot/2, A_tot+margin*0.1, txt, ha='center', va='bottom', color='green', fontweight='bold', bbox=dict(facecolor='white', ec='green', boxstyle='round,pad=0.2'))
|
| 457 |
|
| 458 |
-
ax.text(L_tot/2, -margin/2, f"L: {L_tot:.1f} mm", ha='center', va='center', fontweight='bold')
|
| 459 |
ax.text(-margin/2, A_tot/2, f"A: {A_tot:.1f} mm", ha='center', va='center', fontweight='bold', rotation=90)
|
| 460 |
|
| 461 |
if is_galv:
|
|
@@ -479,24 +422,29 @@ def plot_side_view(info):
|
|
| 479 |
|
| 480 |
if is_galv:
|
| 481 |
layers = info['ga'] if not info['sext'] else len(info['layout'])
|
| 482 |
-
|
| 483 |
for i in range(1, layers):
|
| 484 |
-
if info['sext']
|
| 485 |
-
|
| 486 |
-
|
|
|
|
| 487 |
else:
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
-
|
| 494 |
-
for idx, p in enumerate(
|
| 495 |
x = c * p
|
| 496 |
if not is_galv and idx == 2:
|
| 497 |
-
|
| 498 |
else:
|
| 499 |
-
|
| 500 |
|
| 501 |
ax.text(c/2, -h*0.2, f"Comp: {c:.0f} mm", ha='center', va='top', fontweight='bold')
|
| 502 |
ax.set_xlim(-c*0.05, c*1.05); ax.set_ylim(-h*0.3, h*1.1); ax.axis('off')
|
|
@@ -518,10 +466,10 @@ def generate_pdf(res):
|
|
| 518 |
story.append(Paragraph(f"Ficha: {m['tipo']} {m['l']}x{m['a']} #{m['e']}", styles['Title']))
|
| 519 |
story.append(Spacer(1, 12))
|
| 520 |
acab = "Galvanizado (Com Madeira)" if m['is_galv'] else "Preto (Normal)"
|
| 521 |
-
fita = f"4x {m['fita_nome']} ({m['
|
| 522 |
-
if m['
|
| 523 |
-
else: fita = f"5x {m['fita_nome']} ({m['
|
| 524 |
-
wood = f"{m['qtd_madeiras']} pçs (48.2x6.6mm) | Ref: {m.get('
|
| 525 |
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]]
|
| 526 |
t = Table(data, colWidths=[150, 300])
|
| 527 |
t.setStyle(TableStyle([('GRID', (0,0), (-1,-1), 1, colors.black), ('BACKGROUND', (0,0), (0,-1), colors.lightgrey)]))
|
|
@@ -543,62 +491,51 @@ def generate_csv(res):
|
|
| 543 |
m = res['main']
|
| 544 |
writer.writerow(["Tipo", "L", "A", "E", "Comp", "Acabamento", "Qtd Pacotes", "Pecas/Pct", "Peso Total", "Fita Principal", "Fita Meio", "Qtd Madeira", "Tam Madeira"])
|
| 545 |
acab = "GI/GA" if m['is_galv'] else "Normal"
|
| 546 |
-
writer.writerow([m['tipo'], m['l'], m['a'], m['e'], m['c'], acab, res['total_pcts'], m['num'], f"{res['peso_total']:.2f}", m['fita_nome'], m['fita_meio_nome'], m['qtd_madeiras'], m.get('
|
| 547 |
return output.getvalue()
|
| 548 |
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
if
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
t1, t2 = st.tabs(["Visual", "Downloads"])
|
| 592 |
-
with t1:
|
| 593 |
-
c_g, c_s = st.columns([1,1])
|
| 594 |
-
with c_g: st.pyplot(plot_cross_section(m))
|
| 595 |
-
with c_s: st.pyplot(plot_side_view(m))
|
| 596 |
-
with t2:
|
| 597 |
-
st.download_button("📄 PDF", generate_pdf(res), "relatorio.pdf", "application/pdf")
|
| 598 |
-
st.download_button("📊 CSV", generate_csv(res), "dados.csv", "text/csv")
|
| 599 |
-
|
| 600 |
-
with aba_admin:
|
| 601 |
-
st.info("Edição de Catálogo JSON")
|
| 602 |
col_a, col_b = st.columns(2)
|
| 603 |
with col_a:
|
| 604 |
adm_tipo = st.selectbox("Norma", ["IT14", "IT13"])
|
|
@@ -610,7 +547,6 @@ with aba_admin:
|
|
| 610 |
adm_peso = st.number_input("Peso", value=0.0)
|
| 611 |
adm_ml = st.number_input("Colunas", value=0)
|
| 612 |
adm_ma = st.number_input("Linhas", value=0)
|
| 613 |
-
|
| 614 |
if st.button("Salvar"):
|
| 615 |
new = {"ml": int(adm_ml), "ma": int(adm_ma), "qtde": int(adm_qtde), "peso": float(adm_peso)}
|
| 616 |
key = f"{adm_l:.2f}_{adm_e:.2f}" if adm_tipo == "IT13" else f"{adm_l:.2f}_{adm_a:.2f}_{adm_e:.2f}"
|
|
|
|
| 12 |
from reportlab.lib.pagesizes import letter
|
| 13 |
|
| 14 |
# ==========================================
|
| 15 |
+
# CONFIGURAÇÃO GERAL
|
| 16 |
# ==========================================
|
| 17 |
+
st.set_page_config(page_title="SGP - Gestão de Embalagem v12.1", layout="wide", page_icon="🏭")
|
| 18 |
|
|
|
|
| 19 |
PESO_IDEAL_MIN = 970
|
| 20 |
PESO_IDEAL_MAX = 1080
|
| 21 |
PESO_LIMITE_SEGURANCA = 1200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
FITAS = [
|
| 24 |
{"nome": "Fita 1.20", "tamanho": 1.20}, {"nome": "Fita 1.40", "tamanho": 1.40},
|
|
|
|
| 29 |
{"nome": "Fita 3.50", "tamanho": 3.50}, {"nome": "Fita 4.00", "tamanho": 4.00}
|
| 30 |
]
|
| 31 |
|
| 32 |
+
MADEIRAS_PADRAO = [370, 400, 500, 600, 800, 1000]
|
| 33 |
+
|
| 34 |
DB_FILE = 'catalogo.json'
|
| 35 |
|
|
|
|
|
|
|
|
|
|
| 36 |
@st.cache_data
|
| 37 |
+
def load_db():
|
| 38 |
+
if not os.path.exists(DB_FILE): return {"IT13": {}, "IT14": {}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
try:
|
| 40 |
+
with open(DB_FILE, 'r') as f: return json.load(f)
|
| 41 |
+
except: return {"IT13": {}, "IT14": {}}
|
| 42 |
+
|
| 43 |
+
def save_db(new_db):
|
| 44 |
+
with open(DB_FILE, 'w') as f: json.dump(new_db, f, indent=4)
|
| 45 |
+
st.cache_data.clear()
|
| 46 |
|
| 47 |
DB = load_db()
|
| 48 |
|
| 49 |
# ==========================================
|
| 50 |
+
# LÓGICA DE CÁLCULO
|
| 51 |
# ==========================================
|
| 52 |
|
| 53 |
class PackageCalculator:
|
| 54 |
+
def _parse_layout(self, layout_str):
|
| 55 |
try:
|
| 56 |
return [int(x) for x in layout_str.split('-') if x.strip().isdigit()]
|
| 57 |
except:
|
| 58 |
return []
|
| 59 |
|
| 60 |
+
def _find_smart_neighbor(self, tipo, l, a, e):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
best_item = None
|
| 62 |
+
min_diff = float('inf')
|
|
|
|
| 63 |
target_db = DB["IT13"] if tipo == "Redondo" else DB["IT14"]
|
| 64 |
|
| 65 |
for key, item in target_db.items():
|
| 66 |
try:
|
| 67 |
parts = [float(x) for x in key.split('_')]
|
| 68 |
if tipo == "Redondo":
|
| 69 |
+
# Mesmo diametro, espessura diferente
|
| 70 |
+
if abs(l - parts[0]) < 0.1:
|
| 71 |
+
diff = abs(e - parts[1])
|
| 72 |
+
if diff < min_diff:
|
| 73 |
+
min_diff = diff
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
best_item = item
|
| 75 |
else:
|
| 76 |
+
# Mesma geometria
|
| 77 |
+
if (abs(l - parts[0]) < 0.1 and abs(a - parts[1]) < 0.1) or \
|
| 78 |
+
(abs(l - parts[1]) < 0.1 and abs(a - parts[0]) < 0.1):
|
| 79 |
+
diff = abs(e - parts[2])
|
| 80 |
+
if diff < min_diff:
|
| 81 |
+
min_diff = diff
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
best_item = item
|
| 83 |
+
except: continue
|
|
|
|
| 84 |
return best_item
|
| 85 |
|
| 86 |
+
def _escolher_fita(self, perimetro_m):
|
| 87 |
+
perimetro_com_margem = perimetro_m * 1.10
|
| 88 |
+
for f in FITAS:
|
| 89 |
+
if f["tamanho"] >= perimetro_com_margem:
|
| 90 |
+
return f["nome"], f["tamanho"], perimetro_com_margem
|
| 91 |
+
return "Sob Medida", perimetro_com_margem, perimetro_com_margem
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
def _escolher_madeira(self, largura_pacote_mm):
|
| 94 |
+
for m in MADEIRAS_PADRAO:
|
| 95 |
+
if m >= largura_pacote_mm: return m
|
| 96 |
+
return MADEIRAS_PADRAO[-1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
def calculate(self, modo, meta, l, a, e, c, tipo, acabamento, aba=0):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
l, a, e = float(l), float(a), float(e)
|
| 100 |
is_galv = (acabamento == "Galvanizado (GI/GA)")
|
| 101 |
+
wood_h = 6.60 if is_galv else 0.0
|
| 102 |
+
wood_w = 48.20
|
| 103 |
+
|
| 104 |
key_red = f"{l:.2f}_{e:.2f}"
|
| 105 |
key_met = f"{l:.2f}_{a:.2f}_{e:.2f}"
|
| 106 |
key_met_inv = f"{a:.2f}_{l:.2f}_{e:.2f}"
|
| 107 |
+
|
| 108 |
item = None
|
| 109 |
if tipo == "Redondo":
|
| 110 |
item = DB["IT13"].get(key_red)
|
| 111 |
else:
|
| 112 |
item = DB["IT14"].get(key_met) or DB["IT14"].get(key_met_inv)
|
| 113 |
|
| 114 |
+
densidade = 7.85e-6
|
| 115 |
+
if tipo == "Redondo":
|
| 116 |
+
area = math.pi * ((l/2)**2 - (l/2-e)**2)
|
| 117 |
+
elif tipo == "Enrijecido":
|
| 118 |
+
area = (l + 2*a + 2*aba - 4*e) * e
|
| 119 |
+
else:
|
| 120 |
+
area = (l + 2*a - 2*e) * e
|
| 121 |
+
peso_unit = area * c * densidade
|
| 122 |
|
| 123 |
+
override_rule = False
|
| 124 |
+
gl, ga = 0, 0
|
| 125 |
+
is_sext = False
|
| 126 |
+
layout = None
|
| 127 |
origem = ""
|
| 128 |
+
layout_str = ""
|
| 129 |
+
|
| 130 |
+
# Regra GI/GA
|
| 131 |
+
if is_galv and tipo != "Redondo":
|
| 132 |
+
forced_gl = 0
|
| 133 |
+
if e <= 1.25: forced_gl = 10; origem = "Regra GI/GA (<=1.25mm)"
|
| 134 |
+
elif 1.55 <= e <= 1.95: forced_gl = 8; origem = "Regra GI/GA (1.55-1.95mm)"
|
| 135 |
+
|
| 136 |
+
if forced_gl > 0:
|
| 137 |
+
override_rule = True
|
| 138 |
+
gl = forced_gl
|
| 139 |
+
target_weight = 1000
|
| 140 |
+
total_pcs_pkg = int(target_weight / peso_unit) if peso_unit > 0 else 1
|
| 141 |
+
ga = math.ceil(total_pcs_pkg / gl)
|
| 142 |
+
num_pecas = gl * ga
|
| 143 |
+
is_sext = False
|
| 144 |
+
|
| 145 |
+
if not override_rule:
|
| 146 |
if item:
|
| 147 |
origem = "Catálogo (Norma)"
|
| 148 |
+
num_pecas = item['qtde']
|
| 149 |
+
|
| 150 |
+
# --- CORREÇÃO CRÍTICA AQUI ---
|
| 151 |
+
# Verifica se é um Redondo que se comporta como GRADE (Retangular)
|
| 152 |
+
# Ex: 25.40 -> ml=15, ma=16, total=240. 15*16 = 240. Isso é grade.
|
| 153 |
+
is_grid_match = False
|
| 154 |
+
if item.get('ml') and item.get('ma'):
|
| 155 |
+
if item['ml'] * item['ma'] == item['qtde']:
|
| 156 |
+
is_grid_match = True
|
| 157 |
+
|
| 158 |
+
if item.get('sextavado') and 'padrao' in item:
|
| 159 |
+
# Sextavado Explicito
|
| 160 |
+
layout = self._parse_layout(item.get('padrao', ''))
|
| 161 |
+
is_sext, gl, ga = True, 0, len(layout)
|
| 162 |
+
layout_str = item.get('padrao', '')
|
| 163 |
+
elif tipo == "Redondo" and not is_grid_match:
|
| 164 |
+
# Redondo Sem Grade Definida -> Força Ninho
|
| 165 |
+
base_width = item.get('ml', 0)
|
| 166 |
+
if base_width == 0: base_width = int(math.sqrt(num_pecas))
|
| 167 |
+
layout = []
|
| 168 |
+
rem = num_pecas
|
| 169 |
+
curr_w = base_width
|
| 170 |
+
while rem > 0:
|
| 171 |
+
take = min(rem, curr_w)
|
| 172 |
+
layout.append(take)
|
| 173 |
+
rem -= take
|
| 174 |
+
curr_w = base_width - 1 if curr_w == base_width else base_width
|
| 175 |
+
is_sext, gl, ga = True, 0, len(layout)
|
| 176 |
+
layout_str = "-".join(map(str, layout))
|
| 177 |
+
else:
|
| 178 |
+
# É Metalon ou Redondo com Grade Definida (240 pçs = 15x16)
|
| 179 |
+
layout = None
|
| 180 |
+
is_sext = False
|
| 181 |
+
gl, ga = item.get('ml', 0), item.get('ma', 0)
|
| 182 |
+
|
| 183 |
else:
|
| 184 |
+
# Fallback Engenharia
|
| 185 |
+
# Tenta vizinho primeiro
|
| 186 |
+
neighbor = self._find_smart_neighbor(tipo, l, a, e)
|
| 187 |
+
if neighbor:
|
| 188 |
origem = "Engenharia (Similar)"
|
| 189 |
+
# Copia estrutura do vizinho
|
| 190 |
+
if neighbor.get('sextavado'):
|
| 191 |
+
layout = self._parse_layout(neighbor.get('padrao', ''))
|
| 192 |
+
is_sext, gl, ga = True, 0, len(layout)
|
| 193 |
+
num_pecas = sum(layout)
|
| 194 |
+
else:
|
| 195 |
+
is_sext = False
|
| 196 |
+
gl, ga = neighbor.get('ml', 0), neighbor.get('ma', 0)
|
| 197 |
+
num_pecas = gl * ga
|
| 198 |
else:
|
| 199 |
+
# Calculo Puro
|
| 200 |
+
origem = "Engenharia (Cálculo)"
|
| 201 |
+
peso_alvo = 1000
|
| 202 |
+
num_pecas = int(peso_alvo / peso_unit) if peso_unit > 0 else 1
|
| 203 |
+
|
| 204 |
+
if tipo == "Redondo":
|
| 205 |
+
# Redondo calculado sempre sugere Ninho para segurança
|
| 206 |
+
side = int(math.sqrt(num_pecas/3))
|
| 207 |
+
base = side + 2
|
| 208 |
+
layout = []
|
| 209 |
+
curr = 0
|
| 210 |
+
for i in range(50):
|
| 211 |
+
cols = base if i%2==0 else base-1
|
| 212 |
+
layout.append(cols); curr+=cols
|
| 213 |
+
if curr>=num_pecas: num_pecas=curr; break
|
| 214 |
+
is_sext, gl, ga = True, 0, len(layout)
|
| 215 |
+
else:
|
| 216 |
+
is_sext = False
|
| 217 |
+
gl = int(math.sqrt(num_pecas))
|
| 218 |
+
ga = math.ceil(num_pecas/gl) if gl>0 else 1
|
| 219 |
|
| 220 |
peso_pct = num_pecas * peso_unit
|
| 221 |
|
|
|
|
| 222 |
if modo == "Toneladas":
|
| 223 |
total_pcs = int((meta * 1000) / peso_unit) if peso_unit > 0 else 0
|
| 224 |
else:
|
|
|
|
| 227 |
qtd_pcts = total_pcs // num_pecas if num_pecas > 0 else 0
|
| 228 |
sobra = total_pcs % num_pecas
|
| 229 |
|
| 230 |
+
main = self._build_pkg(l, a, e, c, tipo, aba, num_pecas, peso_pct, origem, is_sext, layout, gl, ga, False, is_galv, wood_h, wood_w, layout_str)
|
|
|
|
| 231 |
|
| 232 |
sobra_pkg = None
|
| 233 |
if sobra > 0:
|
|
|
|
| 238 |
cols = base if k%2==0 else base-1
|
| 239 |
take = min(rem, cols); s_layout.append(take); rem-=take
|
| 240 |
if rem<=0: break
|
| 241 |
+
sobra_pkg = self._build_pkg(l, a, e, c, tipo, aba, sobra, sobra*peso_unit, "Sobra", True, s_layout, 0, len(s_layout), True, is_galv, wood_h, "Sobra")
|
| 242 |
else:
|
| 243 |
s_ga = math.ceil(sobra / gl) if gl>0 else 1
|
| 244 |
+
sobra_pkg = self._build_pkg(l, a, e, c, tipo, aba, sobra, sobra*peso_unit, "Sobra", False, None, gl, s_ga, True, is_galv, wood_h, "")
|
| 245 |
|
| 246 |
return {
|
| 247 |
+
"main": main, "sobra": sobra_pkg,
|
| 248 |
"total_pcts": qtd_pcts, "peso_total": (qtd_pcts * peso_pct) + (sobra_pkg['peso'] if sobra_pkg else 0)
|
| 249 |
}
|
| 250 |
|
| 251 |
+
def _build_pkg(self, l, a, e, c, tipo, aba, num, peso, orig, sext, layout, gl, ga, is_rem, is_galv, wood_h, layout_str):
|
| 252 |
info = {
|
| 253 |
"l": l, "a": a, "e": e, "c": c, "tipo": tipo, "aba": aba,
|
| 254 |
"num": num, "peso": peso, "origem": orig,
|
|
|
|
| 257 |
"layout_str": layout_str
|
| 258 |
}
|
| 259 |
|
| 260 |
+
qtd_madeiras = 0
|
| 261 |
if sext:
|
| 262 |
rows = layout or []
|
| 263 |
max_c = max(rows) if rows else 1
|
| 264 |
info["larg_total"] = max_c * l
|
| 265 |
h_geo = l + (len(rows) - 1) * l * math.sin(math.radians(60))
|
| 266 |
if is_galv:
|
| 267 |
+
camadas_madeira = max(0, len(rows) - 1)
|
| 268 |
+
h_wood = camadas_madeira * wood_h
|
| 269 |
+
qtd_madeiras = camadas_madeira * 5
|
| 270 |
else:
|
| 271 |
+
h_wood = 0
|
| 272 |
+
info["alt_total"] = h_geo + h_wood
|
| 273 |
else:
|
| 274 |
+
# Grade (Retangular)
|
| 275 |
info["larg_total"] = gl * l
|
| 276 |
if tipo == "Enrijecido": info["larg_total"] += aba
|
| 277 |
|
| 278 |
+
# Altura: Se for Redondo em Grade, ele fica um sobre o outro (altura = N * diam)
|
| 279 |
+
# Pois "Embalagem Retangular" sugere alinhamento
|
| 280 |
+
h_base = ga * a if tipo != "Redondo" else ga * l
|
|
|
|
| 281 |
|
| 282 |
if is_galv:
|
| 283 |
+
camadas_madeira = max(0, ga - 1)
|
| 284 |
+
h_wood = camadas_madeira * wood_h
|
| 285 |
+
qtd_madeiras = camadas_madeira * 5
|
| 286 |
else:
|
| 287 |
+
h_wood = 0
|
| 288 |
+
info["alt_total"] = h_base + h_wood
|
| 289 |
|
|
|
|
| 290 |
if is_galv:
|
| 291 |
+
info["madeira_comprimento_real"] = self._escolher_madeira(info["larg_total"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
else:
|
| 293 |
+
info["madeira_comprimento_real"] = 0
|
| 294 |
|
| 295 |
+
info["qtd_madeiras"] = qtd_madeiras
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
+
perim_total = (info["larg_total"] + info["alt_total"]) * 2 / 1000
|
| 298 |
+
nome_fita, tam_fita, real_fita = self._escolher_fita(perim_total)
|
| 299 |
+
info["fita_nome"] = nome_fita
|
| 300 |
+
info["fita_tamanho"] = tam_fita
|
| 301 |
+
|
| 302 |
+
info["fita_meio_nome"] = "N/A"
|
| 303 |
+
info["fita_meio_tamanho"] = 0
|
| 304 |
+
|
| 305 |
if not is_galv:
|
| 306 |
+
perim_meio = (info["larg_total"] + (info["alt_total"] / 2)) * 2 / 1000
|
| 307 |
+
n_meio, t_meio, r_meio = self._escolher_fita(perim_meio)
|
| 308 |
+
info["fita_meio_nome"] = n_meio
|
| 309 |
+
info["fita_meio_tamanho"] = t_meio
|
|
|
|
|
|
|
| 310 |
info["desc_fitas"] = "4 Fitas Totais + 1 Fita Meio"
|
| 311 |
else:
|
| 312 |
info["desc_fitas"] = "5 Fitas Totais (Pacote Completo)"
|
|
|
|
| 314 |
return info
|
| 315 |
|
| 316 |
# ==========================================
|
| 317 |
+
# VISUALIZAÇÃO
|
| 318 |
# ==========================================
|
| 319 |
|
| 320 |
def plot_cross_section(info):
|
| 321 |
fig, ax = plt.subplots(figsize=(10, 8))
|
| 322 |
l, a, e = info['l'], info['a'], info['e']
|
| 323 |
+
wood_h, wood_w = info['wood_h'], info['wood_w']
|
| 324 |
is_galv = info['is_galv']
|
| 325 |
+
wood_len = info.get("madeira_comprimento_real", 0)
|
| 326 |
|
| 327 |
fc = 'lightblue' if not info['is_rem'] else 'lightcoral'
|
| 328 |
ec = 'black'
|
| 329 |
total_w_pkg = info['larg_total']
|
| 330 |
|
| 331 |
if info['sext']:
|
| 332 |
+
# SEXTAVADO
|
| 333 |
rows = info['layout']
|
| 334 |
radius = l / 2
|
| 335 |
dy = l * 0.866
|
|
|
|
| 339 |
y_base = radius + i * dy + (i * wood_h)
|
| 340 |
if is_galv and i > 0:
|
| 341 |
wy = y_base - radius - wood_h
|
| 342 |
+
wood_x_start = (total_w_pkg - wood_len) / 2
|
| 343 |
+
ax.add_patch(patches.Rectangle((wood_x_start, wy), wood_len, wood_h, fc='peru', ec='brown'))
|
| 344 |
for k in range(count):
|
| 345 |
cx = x0 + k * l + radius
|
| 346 |
ax.add_patch(patches.Circle((cx, y_base), radius, fc=fc, ec=ec))
|
| 347 |
ax.add_patch(patches.Circle((cx, y_base), radius-e, fc='white', ec=ec, lw=0.5))
|
| 348 |
else:
|
| 349 |
+
# GRADE RETANGULAR (Corrigido para alinhar Redondo)
|
| 350 |
gl, ga = info['gl'], info['ga']
|
| 351 |
drawn = 0
|
| 352 |
+
|
|
|
|
| 353 |
for i in range(ga):
|
| 354 |
+
# Cálculo de Y (Baseado na camada)
|
| 355 |
if info['tipo'] == "Redondo":
|
| 356 |
+
y = i * (l + wood_h)
|
| 357 |
+
# Centro do círculo será y + raio
|
| 358 |
+
cy = y + (l/2)
|
| 359 |
else:
|
| 360 |
y = i * (a + wood_h)
|
| 361 |
+
cy = y # Metalon desenha do canto
|
| 362 |
+
|
| 363 |
+
# Madeira
|
| 364 |
+
if is_galv and i > 0:
|
| 365 |
+
wy = y - wood_h
|
| 366 |
+
wood_x_start = (total_w_pkg - wood_len) / 2
|
| 367 |
+
ax.add_patch(patches.Rectangle((wood_x_start, wy), wood_len, wood_h, fc='peru', ec='brown'))
|
| 368 |
|
| 369 |
for j in range(gl):
|
| 370 |
if drawn >= info['num']: break
|
| 371 |
+
|
| 372 |
+
x = j * l
|
| 373 |
if info['tipo'] == "Redondo":
|
| 374 |
+
# Redondo em Grade -> Um sobre o outro (alinhado)
|
| 375 |
+
cx = x + (l/2)
|
| 376 |
+
ax.add_patch(patches.Circle((cx, cy), l/2, fc=fc, ec=ec))
|
| 377 |
+
ax.add_patch(patches.Circle((cx, cy), (l/2)-e, fc='white', ec=ec, lw=0.5))
|
|
|
|
|
|
|
| 378 |
else:
|
|
|
|
| 379 |
if info['tipo'] == "Enrijecido":
|
| 380 |
+
ax.add_patch(patches.Rectangle((x, y), l, a, fc='lightcyan', ec=ec))
|
| 381 |
+
ax.add_patch(patches.Rectangle((x+e, y+a/2), info['aba'], e, fc='gray'))
|
| 382 |
else:
|
| 383 |
+
ax.add_patch(patches.Rectangle((x, y), l, a, fc=fc, ec=ec))
|
| 384 |
inset = min(l, a)*0.1
|
| 385 |
+
ax.add_patch(patches.Rectangle((x+inset, y+inset), l-2*inset, a-2*inset, fc='white', ec=ec, lw=0.5))
|
| 386 |
drawn += 1
|
| 387 |
|
| 388 |
L_tot, A_tot = info['larg_total'], info['alt_total']
|
|
|
|
| 392 |
ax.add_patch(patches.Rectangle((-2, -2), L_tot+4, A_tot+4, fill=False, ec='red', ls='--', lw=1.5, label='Limite'))
|
| 393 |
|
| 394 |
if not info['sext']:
|
| 395 |
+
ax.text(L_tot/2, A_tot + margin*0.1, f"BASE: {info['gl']} PEÇAS", ha='center', va='bottom', fontweight='bold', color='blue', fontsize=12, bbox=dict(facecolor='white', edgecolor='blue', boxstyle='round,pad=0.3'))
|
| 396 |
+
ax.text(-margin*0.1, A_tot/2, f"ALTURA: {info['ga']} CAMADAS", ha='right', va='center', fontweight='bold', color='blue', fontsize=12, rotation=90, bbox=dict(facecolor='white', edgecolor='blue', boxstyle='round,pad=0.3'))
|
| 397 |
else:
|
| 398 |
txt = info.get('layout_str') or f"Sextavado ({len(info['layout'])} camadas)"
|
| 399 |
+
ax.text(L_tot/2, A_tot + margin*0.1, txt, ha='center', va='bottom', color='green', fontweight='bold', bbox=dict(facecolor='white', ec='green', boxstyle='round,pad=0.2'))
|
| 400 |
|
| 401 |
+
ax.text(L_tot/2, -margin/2, f"L Tubos: {L_tot:.1f} mm", ha='center', va='center', fontweight='bold')
|
| 402 |
ax.text(-margin/2, A_tot/2, f"A: {A_tot:.1f} mm", ha='center', va='center', fontweight='bold', rotation=90)
|
| 403 |
|
| 404 |
if is_galv:
|
|
|
|
| 422 |
|
| 423 |
if is_galv:
|
| 424 |
layers = info['ga'] if not info['sext'] else len(info['layout'])
|
| 425 |
+
positions_wood = [0.1, 0.3, 0.5, 0.7, 0.9]
|
| 426 |
for i in range(1, layers):
|
| 427 |
+
if info['sext']:
|
| 428 |
+
radius = info['l'] / 2
|
| 429 |
+
dy = info['l'] * 0.866
|
| 430 |
+
y_wood = (radius + i * dy + (i * wood_h)) - radius - wood_h
|
| 431 |
else:
|
| 432 |
+
# Grade
|
| 433 |
+
y_wood = i * (info['a'] + wood_h) - wood_h
|
| 434 |
+
if info['tipo'] == "Redondo": # Redondo grade tem altura D
|
| 435 |
+
y_wood = i * (info['l'] + wood_h) - wood_h
|
| 436 |
+
|
| 437 |
+
for p in positions_wood:
|
| 438 |
+
wx = (c * p) - (wood_w / 2)
|
| 439 |
+
ax.add_patch(patches.Rectangle((wx, y_wood), wood_w, wood_h, fc='peru', ec='brown'))
|
| 440 |
|
| 441 |
+
positions_fita = [0.1, 0.3, 0.5, 0.7, 0.9]
|
| 442 |
+
for idx, p in enumerate(positions_fita):
|
| 443 |
x = c * p
|
| 444 |
if not is_galv and idx == 2:
|
| 445 |
+
ax.plot([x, x], [0, h/2], color='orange', lw=3, ls='--')
|
| 446 |
else:
|
| 447 |
+
ax.plot([x, x], [0, h], color='purple', lw=3)
|
| 448 |
|
| 449 |
ax.text(c/2, -h*0.2, f"Comp: {c:.0f} mm", ha='center', va='top', fontweight='bold')
|
| 450 |
ax.set_xlim(-c*0.05, c*1.05); ax.set_ylim(-h*0.3, h*1.1); ax.axis('off')
|
|
|
|
| 466 |
story.append(Paragraph(f"Ficha: {m['tipo']} {m['l']}x{m['a']} #{m['e']}", styles['Title']))
|
| 467 |
story.append(Spacer(1, 12))
|
| 468 |
acab = "Galvanizado (Com Madeira)" if m['is_galv'] else "Preto (Normal)"
|
| 469 |
+
fita = f"4x {m['fita_nome']} ({m['fita_tamanho']}m)"
|
| 470 |
+
if m['fita_meio_tamanho'] > 0: fita += f" + 1x {m['fita_meio_nome']} ({m['fita_meio_tamanho']}m)"
|
| 471 |
+
else: fita = f"5x {m['fita_nome']} ({m['fita_tamanho']}m)"
|
| 472 |
+
wood = f"{m['qtd_madeiras']} pçs (48.2x6.6mm) | Ref: {m.get('madeira_comprimento_real', 'N/A')}mm" if m['is_galv'] else "N/A"
|
| 473 |
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]]
|
| 474 |
t = Table(data, colWidths=[150, 300])
|
| 475 |
t.setStyle(TableStyle([('GRID', (0,0), (-1,-1), 1, colors.black), ('BACKGROUND', (0,0), (0,-1), colors.lightgrey)]))
|
|
|
|
| 491 |
m = res['main']
|
| 492 |
writer.writerow(["Tipo", "L", "A", "E", "Comp", "Acabamento", "Qtd Pacotes", "Pecas/Pct", "Peso Total", "Fita Principal", "Fita Meio", "Qtd Madeira", "Tam Madeira"])
|
| 493 |
acab = "GI/GA" if m['is_galv'] else "Normal"
|
| 494 |
+
writer.writerow([m['tipo'], m['l'], m['a'], m['e'], m['c'], acab, res['total_pcts'], m['num'], f"{res['peso_total']:.2f}", m['fita_nome'], m['fita_meio_nome'], m['qtd_madeiras'], m.get('madeira_comprimento_real', 0)])
|
| 495 |
return output.getvalue()
|
| 496 |
|
| 497 |
+
st.title("🏭 Calculadora Formadoras v12.1")
|
| 498 |
+
with st.sidebar:
|
| 499 |
+
st.header("Parâmetros")
|
| 500 |
+
tipo = st.selectbox("Perfil", ["Quadrado/Retangular", "Redondo", "U/Enrijecido"])
|
| 501 |
+
acabamento = st.radio("Acabamento", ["Preto (Normal)", "Galvanizado (GI/GA)"])
|
| 502 |
+
c1, c2 = st.columns(2)
|
| 503 |
+
l = c1.number_input("L/Diam", value=50.0)
|
| 504 |
+
a = c2.number_input("Altura", value=50.0, disabled=(tipo=="Redondo"))
|
| 505 |
+
c3, c4 = st.columns(2)
|
| 506 |
+
e = c3.number_input("Espessura", value=2.00)
|
| 507 |
+
c = c4.number_input("Comp", value=6000)
|
| 508 |
+
aba = st.number_input("Aba (U)", value=0.0, disabled=(tipo!="U/Enrijecido"))
|
| 509 |
+
meta = st.number_input("Meta", value=1000)
|
| 510 |
+
modo = st.selectbox("Unidade", ["Peças", "Toneladas"])
|
| 511 |
+
btn = st.button("CALCULAR", type="primary")
|
| 512 |
+
|
| 513 |
+
if btn:
|
| 514 |
+
calc = PackageCalculator()
|
| 515 |
+
t_map = "Enrijecido" if tipo == "U/Enrijecido" else tipo
|
| 516 |
+
if tipo == "Quadrado/Retangular": t_map = "Quadrado/Retangular"
|
| 517 |
+
res = calc.calculate(modo, meta, l, a, e, c, t_map, acabamento, aba)
|
| 518 |
+
m = res['main']
|
| 519 |
+
|
| 520 |
+
if "Engenharia" in m['origem']: st.warning(f"⚠️ {m['origem']}")
|
| 521 |
+
else: st.success(f"✅ {m['origem']}")
|
| 522 |
+
|
| 523 |
+
k1, k2, k3, k4 = st.columns(4)
|
| 524 |
+
k1.metric("Pacotes", res['total_pcts'])
|
| 525 |
+
k2.metric("Pçs/Pct", m['num'])
|
| 526 |
+
k3.metric("Peso Total", f"{res['peso_total']:.0f}kg")
|
| 527 |
+
k4.metric("Fita", f"{m['fita_tamanho']}m")
|
| 528 |
+
|
| 529 |
+
t1, t2 = st.tabs(["Visual", "Downloads"])
|
| 530 |
+
with t1:
|
| 531 |
+
c_g, c_s = st.columns([1,1])
|
| 532 |
+
with c_g: st.pyplot(plot_cross_section(m))
|
| 533 |
+
with c_s: st.pyplot(plot_side_view(m))
|
| 534 |
+
with t2:
|
| 535 |
+
st.download_button("📄 PDF", generate_pdf(res), "relatorio.pdf", "application/pdf")
|
| 536 |
+
st.download_button("📊 CSV", generate_csv(res), "dados.csv", "text/csv")
|
| 537 |
+
|
| 538 |
+
with st.expander("Admin"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
col_a, col_b = st.columns(2)
|
| 540 |
with col_a:
|
| 541 |
adm_tipo = st.selectbox("Norma", ["IT14", "IT13"])
|
|
|
|
| 547 |
adm_peso = st.number_input("Peso", value=0.0)
|
| 548 |
adm_ml = st.number_input("Colunas", value=0)
|
| 549 |
adm_ma = st.number_input("Linhas", value=0)
|
|
|
|
| 550 |
if st.button("Salvar"):
|
| 551 |
new = {"ml": int(adm_ml), "ma": int(adm_ma), "qtde": int(adm_qtde), "peso": float(adm_peso)}
|
| 552 |
key = f"{adm_l:.2f}_{adm_e:.2f}" if adm_tipo == "IT13" else f"{adm_l:.2f}_{adm_a:.2f}_{adm_e:.2f}"
|