"---\nslug: crear-modelo-con-detail-screen\nalias: \"Crear modelo con detail screen\"\naudience: ai_agent\nversion_hash: b58fe7c3a42cc39d8026444804c2a2b14a6535be\nupdated_at: 2026-04-17T05:51:12.318977+00:00\nsource: https://abior.art/api/artefa/manuales/crear-modelo-con-detail-screen/markdown/\n---\n\n# Crear modelo con detail screen\n\n> Checklist paso a paso para crear un nuevo modelo Django y su pantalla de detalle Android en Abior.\n\nEsta tarea es recurrente y los agentes AI solemos olvidar pasos (cablear DELETE en MainScreen, agregar permisos, etc). Este manual consolida el flujo completo end-to-end verificado contra varios casos reales. Cada capitulo es un paso atomico; seguirlos en orden garantiza que las 4 operaciones crear / leer / editar / eliminar funcionen.\n\n## 1. Django: modelo y migracion\n\n_Crear modelo con las FK obligatorias de permisos y campos de auditoria._\n\n### 1.1 Campos obligatorios\n\nTodo modelo Django nuevo en Abior DEBE tener estos campos:\n\n```python\nid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)\ncreated_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)\nupdated = models.DateTimeField(auto_now=True, blank=True, null=True)\ncreated_by = models.ForeignKey(\n    'perms.Gafete', on_delete=models.SET_NULL, blank=True, null=True,\n    related_name='<app>_<modelos>_created',\n)\nuser_admin = models.ForeignKey(\n    'perms.Gafete', on_delete=models.SET_NULL, blank=True, null=True,\n    default=None, related_name='<app>_<modelos>_admin',\n)\nemprendimiento = models.ForeignKey(\n    Emprendimiento, on_delete=models.SET_NULL, blank=True, null=True,\n)\nalias = models.CharField(max_length=255)\ndesc_breve = models.CharField(max_length=300, blank=True, default='')\ndesc = models.TextField(blank=True, null=True)\n```\n\n**NO uses `created` solo (sin `_at`)** â€” `AbiorCursorPagination` ordena por `-created_at` y genera `FieldError` 500 al agregar filtros.\n\nSi el modelo cuelga de un padre (ej. linea de una orden), el padre debe tener `emprendimiento` para que `_resolve_emprendimiento_from_data` resuelva el gafete correcto.\n\n\n### 1.2 Migracion y autorizacion\n\nAntes de `python manage.py makemigrations` **pedir autorizacion al usuario** (regla del proyecto en Abior-webapp/CLAUDE.md). Ejemplo del mensaje:\n\n> Migracion para agregar modelo X con FKs Y/Z (todos nullable, sin cambios destructivos). Aplico?\n\nDespues de aprobar, ejecutar en orden:\n\n```bash\npython manage.py makemigrations <app>\npython manage.py migrate <app>\n```\n\nLa DB es compartida con produccion â€” la migracion aplicada local YA esta en prod. Por eso la autorizacion es importante.\n\n\n## 2. Django: ViewSet y filtros\n\n_Extender AbiorBaseViewSet con filterset y registrar en API_REGISTRY._\n\n### 2.1 ViewSet minimo\n\nEl ViewSet siempre hereda de `AbiorBaseViewSet` (aplica scoping por emprendimiento + `IsOwnerOrAdmin` + autollena `created_by`/`user_admin` en `perform_create`).\n\n```python\nclass <Modelo>ViewSet(AbiorBaseViewSet):\n    queryset = <Modelo>.objects.all()\n    serializer_class = <Modelo>Serializer\n    filterset_fields = ['emprendimiento', 'status', '<FKs principales>']\n```\n\nNo sobreescribir `permission_classes` salvo caso especial. `filterset_fields` SIEMPRE incluye `emprendimiento` si aplica y los FKs que el cliente va a usar para listas filtradas.\n\n```python\nAPI_REGISTRY = [\n    ('<app>/<modelos>', <Modelo>ViewSet, '<modelo>'),\n]\n```\n\n\n### 2.2 Endpoint path conocido\n\n**Anota el path de tu nuevo endpoint** (ej. `compras/ordenes`) â€” lo necesitaras al cablear el DELETE en Android. Si no lo agregas al mapa del cliente Android, el DELETE ira a un path inexistente y el usuario vera un Toast engaÃ±oso \"No tienes permiso\".\n\n\n## 3. Android: DTO y retrofit\n\n_Agregar DTO en Models.kt y endpoints en AbiorApi.kt._\n\n### 3.1 DTO minimo\n\nEn `app/src/main/java/art/abior/android/data/model/Models.kt`:\n\n```kotlin\ndata class <Modelo>Dto(\n    val id: String,\n    val alias: String,\n    @SerializedName(\"desc_breve\") val descBreve: String? = \"\",\n    val desc: String? = \"\",\n    val emprendimiento: String? = null,\n    @SerializedName(\"created_at\") val createdAt: String? = null,\n)\n```\n\nTodos los campos no requeridos con default `null` / `\"\"`. Gson los llena con valores vacios si faltan en el response.\n\n\n### 3.2 Endpoints retrofit\n\nEn `AbiorApi.kt`:\n\n```kotlin\n@GET(\"<app>/<modelos>/\")\nsuspend fun get<Modelos>(@Query(\"emprendimiento\") empId: String): Response<PaginatedResponse<<Modelo>Dto>>\n\n@GET(\"<app>/<modelos>/{id}/\")\nsuspend fun get<Modelo>(@Path(\"id\") id: String): Response<Map<String, @JvmSuppressWildcards Any?>>\n\n@POST(\"<app>/<modelos>/\")\nsuspend fun create<Modelo>(@Body body: Map<String, @JvmSuppressWildcards Any?>): Response<<Modelo>Dto>\n```\n\nUsar `Map<String, Any?>` para el body + detail evita acoplarse al DTO estricto y permite endpoints que incluyan campos extra (como `emprendimiento_alias` calculado).\n\n\n## 4. Android: DetailScreen\n\n_Crear la pantalla de detalle usando defaultDetailMenuActions._\n\n### 4.1 Composable con top bar\n\nEstructura minima (ver `ui/tasks/ProcedimientoDetailScreen.kt` como referencia):\n\n```kotlin\n@Composable\nfun <Modelo>DetailScreen(\n    <modelo>Id: String,\n    modifier: Modifier = Modifier,\n    onBack: () -> Unit = {},\n    onEdit: () -> Unit = {},\n) {\n    var data by remember { mutableStateOf<Map<String, Any?>>(emptyMap()) }\n    var isLoading by remember { mutableStateOf(true) }\n    var menuExpanded by remember { mutableStateOf(false) }\n\n    LaunchedEffect(<modelo>Id) {\n        // Carga con manejo de errores visible (Toast con modelType + id si falla).\n    }\n\n    Column(modifier.fillMaxSize().background(Abior.BgPrimary)) {\n        Row(...) {\n            IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, ...) }\n            Text(\"Detalle de <Modelo>\", modifier = Modifier.weight(1f))\n            NotificationBellAuto()  // OBLIGATORIO por regla Abior-androidapp/CLAUDE.md\n            Box {\n                IconButton(onClick = { menuExpanded = true }) { Icon(Icons.Outlined.MoreVert, ...) }\n                DetailOptionsMenu(\n                    expanded = menuExpanded, onDismiss = { menuExpanded = false },\n                    actions = defaultDetailMenuActions(\n                        objectId = <modelo>Id, modelName = \"<modelo>\",\n                        shareText = alias, onEdit = onEdit,\n                    ),\n                )\n            }\n        }\n        // ...contenido con scroll y secciones...\n    }\n}\n```\n\n**NUNCA** construir menu manual â€” `defaultDetailMenuActions` ya cablea Permisos, Metadatos, Recordatorio, Favorito, Seguir, Editar, Compartir, Eliminar.\n\n`modelName` pasado aqui debe coincidir con el key del mapa DELETE en MainScreen (siguiente capitulo).\n\n\n### 4.2 Loader generico con toast de error\n\nEl `LaunchedEffect` que carga el detalle DEBE mostrar Toast visible si falla, incluyendo `modelType` y los primeros 8 chars del id:\n\n```kotlin\nLaunchedEffect(<modelo>Id) {\n    try {\n        val resp = withContext(Dispatchers.IO) { ApiClient.getApi().get<Modelo>(<modelo>Id) }\n        if (resp.isSuccessful) data = resp.body() ?: emptyMap()\n        else if (resp != null) {\n            Toast.makeText(context, \"No se pudo cargar <modelo> (${<modelo>Id.take(8)}...) - HTTP ${resp.code()}\", Toast.LENGTH_LONG).show()\n        }\n    } catch (e: Exception) {\n        Toast.makeText(context, \"Error cargando <modelo>: ${e.message}\", Toast.LENGTH_LONG).show()\n    }\n    isLoading = false\n}\n```\n\nEl Toast descriptivo ahorra horas de debugging. Sin el, el usuario ve campos vacios sin explicacion.\n\n\n## 5. Android: Create/Edit\n\n_Agregar modelType a CreateModelScreen y EditModelScreen._\n\n### 5.1 Registrar en MODEL_LABELS y modelApiPaths\n\nEn `ui/shared/CreateModelScreen.kt`:\n\n```kotlin\n// Top del archivo: agregar al mapa\nprivate val MODEL_LABELS = mapOf(\n    ..., \"<modelo>\" to \"<Modelo>\",\n)\n\n// fieldsFor(modelType): agregar caso\n\"<modelo>\" -> listOf(\n    CreateFieldDef(\"alias\", \"Nombre\", Icons.Outlined.Label),\n    CreateFieldDef(\"desc_breve\", \"Descripcion breve\", Icons.Outlined.ShortText),\n    CreateFieldDef(\"desc\", \"Descripcion\", Icons.Outlined.Description, multiline = true),\n    // FK opcional: usa fkEndpoint para dropdown automatico\n    CreateFieldDef(\"otro_modelo\", \"Otro (opcional)\", Icons.Outlined.Label, fkEndpoint = \"<app>/<otros>/\"),\n    // Boolean: flag boolean = true\n    CreateFieldDef(\"activo\", \"Activo\", Icons.Outlined.Check, boolean = true),\n    // Decimal: flag decimal = true\n    CreateFieldDef(\"costo\", \"Precio\", Icons.Outlined.AttachMoney, decimal = true),\n)\n\n// Ramar de creacion (cerca de la linea 1390)\n\"<modelo>\" -> api.create<Modelo>(body)\n```\n\nEn `EditModelScreen.kt` replicar el case en `getFieldDefs`. El loader automatico ya usa esos fieldDefs para precargar valores.\n\n`modelApiPaths` (linea ~80): agregar `\"<modelo>\" to \"<app>/<modelos>\"`.\n\n\n## 6. Android: MainScreen wiring\n\n_El paso mas facil de olvidar: state, BackHandler, render chain, DELETE mapping._\n\n### 6.1 State y BackHandler\n\nEn `ui/main/MainScreen.kt` agregar:\n\n```kotlin\n// 1. State (cerca de los otros selected*Id)\nvar selected<Modelo>Id by remember { mutableStateOf<String?>(null) }\n\n// 2. En el BackHandler enabled, agregar la condicion\nBackHandler(enabled = ... || selected<Modelo>Id != null || ...)\n\n// 3. En el when del BackHandler, agregar (en orden deepest-first, ANTES de selected del padre)\nselected<Modelo>Id != null -> selected<Modelo>Id = null\n\n// 4. En onTabSelected (reset al cambiar bottom tab)\nonTabSelected = { ...; selected<Modelo>Id = null; ... }\n```\n\n\n### 6.2 Render chain\n\nRama condicional para mostrar el detail:\n\n```kotlin\n} else if (selected<Modelo>Id != null) {\n    <Modelo>DetailScreen(\n        <modelo>Id = selected<Modelo>Id!!,\n        modifier = Modifier.padding(padding),\n        onBack = { selected<Modelo>Id = null },\n        onEdit = { editingModel = \"<modelo>\" to selected<Modelo>Id!! },\n    )\n}\n```\n\nOrden en el chain: colocar DESPUES de `editingModel != null` (editor arriba de todo) y DESPUES del padre del modelo si aplica. **Deepest-first**.\n\n\n### 6.3 Mapa DELETE â€” CRITICO\n\nEn el confirmButton del dialog `deleteTarget`, mapa `endpoint` (linea ~231):\n\n```kotlin\nval endpoint = when {\n    ...,\n    model == \"<modelo>\" -> \"<app>/<modelos>\",  // AGREGAR ESTA LINEA\n    ...,\n    else -> model.replace(\".\", \"/\") + \"s\"\n}\n```\n\n**Si no agregas esta linea, el DELETE va a `/api/<modelo>s/{id}/` que NO existe (404). El cliente muestra Toast \"No tienes permiso\" engaÃ±oso** porque el body del 404 contiene la palabra \"permiso\".\n\nUsar `model == \"<modelo>\"` (igualdad exacta) NO `in model` (substring) para evitar falsos positivos.\n\n\n## 7. Verificacion end-to-end\n\n_Ejecutar los 4 flujos del dueÃ±o antes de dar por terminada la feature._\n\n### 7.1 Checklist final\n\nComo dueÃ±o del emprendimiento, validar:\n\n1. **Crear** con el modelo nuevo desde la pantalla relevante. Debe aparecer en la lista.\n2. **Listar** con filtro por emprendimiento â€” el objeto creado aparece.\n3. **Ver detalle** tap en el objeto â€” abre `<Modelo>DetailScreen` con datos precargados. NO muestra Toast de error.\n4. **Editar** desde el menu 3 puntos â€” abre `EditModelScreen` con campos precargados, guarda, regresa y refleja cambios.\n5. **Eliminar** desde el menu 3 puntos â€” confirma, regresa al padre, la lista refresca sin el objeto.\n\n**Si alguno falla con mensaje confuso** (\"No tienes permiso\", \"No encontrado\", campos vacios):\n- Revisar mapa DELETE en MainScreen (bug mas comun).\n- Revisar que `emprendimiento` se envie en el body del create (el scoping lo requiere).\n- Revisar que el path del endpoint coincida con el del registro API_REGISTRY.\n\nCompilar con `./gradlew assembleBetaDebug`, publicar TestVersion siguiendo `Abior-androidapp/docs/instructivo-publicar-version.md` y pushear a rama Test.\n"