tokiospg commited on
Commit
ddb1b40
·
verified ·
1 Parent(s): 1dfc3e9

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +94 -0
app.py CHANGED
@@ -2297,6 +2297,77 @@ LOGISTICS_ROUTE_SCOPE = (
2297
  "Las tarifas actuales están calibradas para envíos desde USA/Miami hacia Panamá/ACP. "
2298
  "Para otros orígenes como China, Europa o México se debe cargar una ruta logística distinta."
2299
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2300
 
2301
  def logistics_calc_summary(freight_row, local_row, peso_libras, incoterm="FOB",
2302
  largo=0, ancho=0, alto=0, unidad_dimensional="in", bultos=1):
@@ -4986,6 +5057,7 @@ Responde de forma clara y profesional. Si puedes dar un número o dato exacto de
4986
  freight_df = get_logistics_freight_rates_cached()
4987
  local_df = get_logistics_local_rates_cached()
4988
  incoterms_df = get_logistics_incoterms_cached()
 
4989
  if freight_df.empty:
4990
  render_empty_state("Sin tarifas logísticas", "Solicita a Logística cargar las tarifas base antes de calcular costos.")
4991
  else:
@@ -5024,6 +5096,7 @@ Responde de forma clara y profesional. Si puedes dar un número o dato exacto de
5024
  with c2:
5025
  agente_flete = st.selectbox("Agente", freight_filtered["agente"].dropna().unique().tolist(), key="rfq_log_agente")
5026
  freight_row = freight_filtered[freight_filtered["agente"] == agente_flete].iloc[0]
 
5027
 
5028
  if modo_peso_rfq == "Por paquete":
5029
  c3, c4, c5, c6 = st.columns([0.24, 0.24, 0.24, 0.28])
@@ -5050,6 +5123,8 @@ Responde de forma clara y profesional. Si puedes dar un número o dato exacto de
5050
  peso_paquete_rfq = peso_unit_lb
5051
  peso_total_lb = peso_unit_lb * cantidad_log
5052
 
 
 
5053
  st.caption("Dimensiones por paquete/bulto para estimar peso volumétrico.")
5054
  d1, d2, d3, d4, d5 = st.columns([0.2, 0.2, 0.2, 0.18, 0.22])
5055
  with d1:
@@ -5116,6 +5191,8 @@ Responde de forma clara y profesional. Si puedes dar un número o dato exacto de
5116
  "peso_por_paquete_lb": peso_paquete_rfq,
5117
  "bultos": calc["bultos"],
5118
  "volumen_pies_cubicos": calc["volumen_pies_cubicos"],
 
 
5119
  "codigo_articulo": str(selected_item.get("codigo_articulo", "")),
5120
  }
5121
  )
@@ -5132,6 +5209,23 @@ Responde de forma clara y profesional. Si puedes dar un número o dato exacto de
5132
  f"{LOGISTICS_ROUTE_LABEL}. {agente_flete} / {tipo_flete}: {calc['tiempo_transito_dias']} día(s). Peso real: {calc['peso_libras']:,.2f} lb. Peso facturable: {calc['peso_facturable_libras']:,.2f} lb.",
5133
  "blue",
5134
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5135
 
5136
  calc_df = get_logistics_calculations_cached(50)
5137
  if not calc_df.empty:
 
2297
  "Las tarifas actuales están calibradas para envíos desde USA/Miami hacia Panamá/ACP. "
2298
  "Para otros orígenes como China, Europa o México se debe cargar una ruta logística distinta."
2299
  )
2300
+ LOGISTICS_STEP_LABELS = {
2301
+ "embalaje_verificacion": "Embalaje y verificación",
2302
+ "carga_almacen": "Carga en almacén",
2303
+ "transporte_interno_origen": "Transporte interno origen",
2304
+ "tramites_aduaneros_exportacion": "Aduana exportación",
2305
+ "costo_terminal_origen": "Terminal origen",
2306
+ "transporte_principal": "Transporte principal",
2307
+ "seguro_transporte": "Seguro transporte",
2308
+ "costo_terminal_destino": "Terminal destino",
2309
+ "tramites_aduaneros_importacion": "Aduana importación",
2310
+ "transporte_interior_destino": "Transporte interior destino",
2311
+ "descarga_almacen_comprador": "Descarga almacén comprador",
2312
+ }
2313
+ LOGISTICS_FORWARDER_ALIASES = {
2314
+ "ABMCARGO": ["ABM LOGISTICS", "ABMCARGO"],
2315
+ "SOUTHCARGO": ["SOUTH CARGO", "SOUTHCARGO", "SOUTH CARGO"],
2316
+ "SOUTH CARGO": ["SOUTH CARGO", "SOUTHCARGO"],
2317
+ }
2318
+
2319
+ def normalize_logistics_name(value):
2320
+ text = str(value or "").upper()
2321
+ text = re.sub(r"[^A-Z0-9]", "", text)
2322
+ return text
2323
+
2324
+ def get_forwarder_for_agent(forwarders_df, agent_name):
2325
+ if forwarders_df is None or forwarders_df.empty:
2326
+ return pd.Series(dtype=object)
2327
+ normalized_agent = normalize_logistics_name(agent_name)
2328
+ candidates = LOGISTICS_FORWARDER_ALIASES.get(normalized_agent, [agent_name])
2329
+ candidate_keys = {normalize_logistics_name(item) for item in candidates}
2330
+ for _, row in forwarders_df.iterrows():
2331
+ row_key = normalize_logistics_name(row.get("nombre", ""))
2332
+ if row_key in candidate_keys or row_key == normalized_agent:
2333
+ return row
2334
+ for _, row in forwarders_df.iterrows():
2335
+ row_key = normalize_logistics_name(row.get("nombre", ""))
2336
+ if normalized_agent and (normalized_agent in row_key or row_key in normalized_agent):
2337
+ return row
2338
+ return pd.Series(dtype=object)
2339
+
2340
+ def build_incoterm_summary(incoterms_df, sigla):
2341
+ if incoterms_df is None or incoterms_df.empty:
2342
+ return "No hay matriz de Incoterms cargada."
2343
+ match = incoterms_df[incoterms_df["sigla"].astype(str).str.upper() == str(sigla or "").upper()]
2344
+ if match.empty:
2345
+ return f"No hay explicación cargada para {sigla}."
2346
+ row = match.iloc[0]
2347
+ responsabilidades = row.get("responsabilidades", {}) or {}
2348
+ if isinstance(responsabilidades, str):
2349
+ try:
2350
+ responsabilidades = json.loads(responsabilidades)
2351
+ except Exception:
2352
+ responsabilidades = {}
2353
+ comprador = [
2354
+ LOGISTICS_STEP_LABELS.get(key, key.replace("_", " ").title())
2355
+ for key, value in responsabilidades.items()
2356
+ if str(value).strip().lower().startswith("comprador")
2357
+ ]
2358
+ vendedor = [
2359
+ LOGISTICS_STEP_LABELS.get(key, key.replace("_", " ").title())
2360
+ for key, value in responsabilidades.items()
2361
+ if str(value).strip().lower().startswith("vendedor")
2362
+ ]
2363
+ incoterm_name = row.get("incoterm", sigla)
2364
+ comprador_text = ", ".join(comprador[:4]) + ("..." if len(comprador) > 4 else "") if comprador else "No especificado"
2365
+ vendedor_text = ", ".join(vendedor[:4]) + ("..." if len(vendedor) > 4 else "") if vendedor else "No especificado"
2366
+ return (
2367
+ f"{sigla} - {incoterm_name}. "
2368
+ f"Proveedor cubre principalmente: {vendedor_text}. "
2369
+ f"Comprador/Proyelec debe estimar: {comprador_text}."
2370
+ )
2371
 
2372
  def logistics_calc_summary(freight_row, local_row, peso_libras, incoterm="FOB",
2373
  largo=0, ancho=0, alto=0, unidad_dimensional="in", bultos=1):
 
5057
  freight_df = get_logistics_freight_rates_cached()
5058
  local_df = get_logistics_local_rates_cached()
5059
  incoterms_df = get_logistics_incoterms_cached()
5060
+ forwarders_df = get_logistics_forwarders_cached()
5061
  if freight_df.empty:
5062
  render_empty_state("Sin tarifas logísticas", "Solicita a Logística cargar las tarifas base antes de calcular costos.")
5063
  else:
 
5096
  with c2:
5097
  agente_flete = st.selectbox("Agente", freight_filtered["agente"].dropna().unique().tolist(), key="rfq_log_agente")
5098
  freight_row = freight_filtered[freight_filtered["agente"] == agente_flete].iloc[0]
5099
+ forwarder_row = get_forwarder_for_agent(forwarders_df, agente_flete)
5100
 
5101
  if modo_peso_rfq == "Por paquete":
5102
  c3, c4, c5, c6 = st.columns([0.24, 0.24, 0.24, 0.28])
 
5123
  peso_paquete_rfq = peso_unit_lb
5124
  peso_total_lb = peso_unit_lb * cantidad_log
5125
 
5126
+ incoterm_summary = build_incoterm_summary(incoterms_df, incoterm_log)
5127
+
5128
  st.caption("Dimensiones por paquete/bulto para estimar peso volumétrico.")
5129
  d1, d2, d3, d4, d5 = st.columns([0.2, 0.2, 0.2, 0.18, 0.22])
5130
  with d1:
 
5191
  "peso_por_paquete_lb": peso_paquete_rfq,
5192
  "bultos": calc["bultos"],
5193
  "volumen_pies_cubicos": calc["volumen_pies_cubicos"],
5194
+ "forwarder_direccion": str(forwarder_row.get("direccion", "")) if not forwarder_row.empty else "",
5195
+ "forwarder_observacion": str(forwarder_row.get("observacion", "")) if not forwarder_row.empty else "",
5196
  "codigo_articulo": str(selected_item.get("codigo_articulo", "")),
5197
  }
5198
  )
 
5209
  f"{LOGISTICS_ROUTE_LABEL}. {agente_flete} / {tipo_flete}: {calc['tiempo_transito_dias']} día(s). Peso real: {calc['peso_libras']:,.2f} lb. Peso facturable: {calc['peso_facturable_libras']:,.2f} lb.",
5210
  "blue",
5211
  )
5212
+ if not forwarder_row.empty:
5213
+ render_notice_panel(
5214
+ "Ubicación del forwarder",
5215
+ f"{forwarder_row.get('nombre', agente_flete)}: {forwarder_row.get('direccion', 'Sin dirección cargada')}. {forwarder_row.get('observacion', '')}",
5216
+ "green",
5217
+ )
5218
+ else:
5219
+ render_notice_panel(
5220
+ "Ubicación del forwarder",
5221
+ "No hay dirección cargada para este agente. Logística debe completar la ficha del forwarder.",
5222
+ "amber",
5223
+ )
5224
+ render_notice_panel(
5225
+ "Incoterm aplicado",
5226
+ incoterm_summary,
5227
+ "amber",
5228
+ )
5229
 
5230
  calc_df = get_logistics_calculations_cached(50)
5231
  if not calc_df.empty: