Spaces:
Runtime error
Runtime error
Upload app.py
Browse files
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:
|