Skip to main content

Overview

The Products & Concepts catalog manages all billable work items in the system, divided into:
  • Ordinary Concepts (Conceptos Ordinarios): Standard contracted work items with fixed pricing
  • Extraordinary Concepts (Conceptos Extraordinarios/PUEs): Special work items requiring authorization

Data Model

ConceptoMaestro

The primary model for all work concepts:
operaciones/models/catalogos_models.py
class ConceptoMaestro(models.Model):
    sub_anexo = models.ForeignKey(SubAnexo, on_delete=models.CASCADE, related_name='conceptos', null=True, blank=True)
    partida_ordinaria = models.CharField(max_length=50, null=True, blank=True)
    codigo_interno = models.CharField(max_length=50, blank=True, null=True)
    descripcion = models.TextField()
    unidad_medida = models.ForeignKey(UnidadMedida, on_delete=models.PROTECT)
    cantidad = models.DecimalField(max_digits=20, decimal_places=2, default=0)
    precio_unitario_mn = models.DecimalField(max_digits=18, decimal_places=2, default=0, null=True, blank=True)
    precio_unitario_usd = models.DecimalField(max_digits=18, decimal_places=2, default=0, null=True, blank=True)
    id_tipo_partida = models.ForeignKey(Tipo, on_delete=models.CASCADE, limit_choices_to={'nivel_afectacion': 3})
    categoria = models.TextField(null=True, blank=True)
    subcategoria = models.TextField(null=True, blank=True)
    clasificacion = models.TextField(null=True, blank=True)

    # Extraordinary item fields
    partida_extraordinaria = models.CharField(max_length=50, null=True, blank=True)
    pte_creacion = models.CharField(max_length=100, null=True, blank=True)
    ot_creacion = models.CharField(max_length=100, null=True, blank=True)
    fecha_autorizacion = models.DateField(null=True, blank=True)
    estatus = models.CharField(max_length=20, blank=True, null=True)

    comentario = models.TextField(blank=True, null=True)
    activo = models.BooleanField(default=True)

    class Meta:
        db_table = 'contrato_concepto_maestro'
        indexes = [
            models.Index(fields=['partida_ordinaria']),
            models.Index(fields=['sub_anexo']),
        ]

    def __str__(self):
        return f"{self.partida_ordinaria} ({self.sub_anexo.clave_anexo})"
class UnidadMedida(models.Model):
    descripcion = models.CharField(max_length=50)
    clave = models.CharField(max_length=10)  # e.g., "M2", "KG", "PZA"
    activo = models.BooleanField(default=True)
    comentario = models.TextField(blank=True, null=True)
    
    class Meta:
        db_table = 'unidad_medida'
class SubAnexo(models.Model):
    anexo_maestro = models.ForeignKey(AnexoContrato, on_delete=models.CASCADE, related_name='sub_anexos')
    clave_anexo = models.CharField(max_length=50)
    descripcion = models.TextField()
    unidad_medida = models.ForeignKey(UnidadMedida, on_delete=models.CASCADE, null=True, blank=True)
    cantidad = models.DecimalField(max_digits=20, decimal_places=2, default=0)
    precio_unitario_mn = models.DecimalField(max_digits=20, decimal_places=2, default=0)
    precio_unitario_usd = models.DecimalField(max_digits=20, decimal_places=2, default=0)
    activo = models.BooleanField(default=True)
    
    class Meta:
        db_table = 'contrato_sub_anexo'
        ordering = ['clave_anexo']
        unique_together = ['anexo_maestro', 'clave_anexo']
class Tipo(models.Model):
    TIPO_CHOICES = [
        ('1', 'PTE'),
        ('2', 'OT'),
        ('3', 'PARTIDA'),
        ('4', 'PRODUCCION')
    ]
    
    descripcion = models.CharField(max_length=200)
    nivel_afectacion = models.IntegerField(choices=TIPO_CHOICES, default=0)
    comentario = models.TextField(blank=True, null=True)
    activo = models.BooleanField(default=True)

Ordinary Concepts

Listing Ordinary Concepts

operaciones/views/catalogos.py
@login_required(login_url='/accounts/login/')
def lista_conceptos_ordinarios(request):
    """Renderiza la página de Conceptos Ordinarios"""
    context = {
        'titulo': 'Catálogo de Conceptos Ordinarios',
        'tipo_vista': 'ordinarios'
    }
    return render(request, 'operaciones/catalogos/producto/lista_producto.html', context)

DataTable API for Ordinary Concepts

operaciones/views/catalogos.py
def datatable_conceptos(request):
    draw = int(request.GET.get('draw', 1))
    start = int(request.GET.get('start', 0))
    length = int(request.GET.get('length', 10))
    search_value = request.GET.get('filtro', '')
    
    modo_vista = request.GET.get('modo_vista', 'ordinarios')
    unidad_medida = request.GET.get('unidad_medida', '')
    
    order_column_index = request.GET.get('order[0][column]', '0')
    order_direction = request.GET.get('order[0][dir]', 'desc')

    column_mapping = {
        '0': 'id',
        '1': 'partida_ordinaria' if modo_vista == 'ordinarios' else 'partida_extraordinaria',
        '2': 'descripcion',
        '3': 'unidad_medida__descripcion',
        '4': 'sub_anexo__clave_anexo',
        '5': 'precio_unitario_mn',
        '6': 'precio_unitario_usd',
        '7': 'activo'
    }

    order_field = column_mapping.get(order_column_index, 'id')
    if order_direction == 'desc':
        order_field = f'-{order_field}'

    conceptos = ConceptoMaestro.objects.select_related(
        'sub_anexo', 
        'unidad_medida', 
        'id_tipo_partida'
    )

    # Filter by ordinary concepts (tipo_partida_id = 6)
    if modo_vista == 'ordinarios':
        conceptos = conceptos.filter(id_tipo_partida_id=6)

    if search_value:
        conceptos = conceptos.filter(
            Q(partida_ordinaria__icontains=search_value) |
            Q(descripcion__icontains=search_value) |
            Q(sub_anexo__clave_anexo__icontains=search_value)
        )

    if unidad_medida:
        conceptos = conceptos.filter(unidad_medida_id=unidad_medida)

    total_records_filtered = conceptos.count()
    conceptos = conceptos.order_by(order_field)[start:start + length]

    data = []
    for c in conceptos:
        anexo_display = c.sub_anexo.clave_anexo if c.sub_anexo else 'S/A'

        data.append({
            'id': c.id,
            'id_partida': c.partida_ordinaria,
            'descripcion': c.descripcion,
            'unidad_medida': c.unidad_medida.clave,
            'anexo': anexo_display,
            'cantidad_referencia': str(c.cantidad),
            'precio_unitario_mn': str(c.precio_unitario_mn),
            'precio_unitario_usd': str(c.precio_unitario_usd),
            'activo': 'Activo' if c.activo else 'Inactivo',
            'activo_bool': c.activo,
            'comentario': c.comentario,
        })

    return JsonResponse({
        'draw': draw,
        'recordsTotal': ConceptoMaestro.objects.count(),
        'recordsFiltered': total_records_filtered,
        'data': data
    })
Type ID 6: Ordinary concepts are identified by id_tipo_partida_id=6 in the database.

Extraordinary Concepts (PUEs)

Listing Extraordinary Concepts

operaciones/views/catalogos.py
@login_required(login_url='/accounts/login/')
def lista_conceptos_extraordinarios(request):
    """Renderiza la página de Conceptos Extraordinarios"""
    context = {
        'titulo': 'Catálogo de Conceptos Extraordinarios',
        'tipo_vista': 'extraordinarios'
    }
    return render(request, 'operaciones/catalogos/producto/lista_producto.html', context)

Creating Extraordinary Concepts

operaciones/views/catalogos.py
@require_http_methods(["POST"])
def crear_producto(request):
    """
    Crea un nuevo concepto extraordinario.
    """
    try:
        id_partida = request.POST.get('id_partida')
        descripcion_concepto = request.POST.get('descripcion')
        unidad_medida_id = request.POST.get('unidad_medida')
        precio_unitario_mn = request.POST.get('precio_unitario_mn', 0)
        precio_unitario_usd = request.POST.get('precio_unitario_usd', 0)
        comentario = request.POST.get('comentario', '')
        pte_origen = request.POST.get('pte_origen', '')
        ot_origen = request.POST.get('ot_origen', '')
        cantidad_post = request.POST.get('cantidad', 0)
        tipo_partida_id = 7  # Extraordinary concept type

        if not id_partida:
            return JsonResponse({
                'exito': False, 
                'tipo_aviso': 'error', 
                'detalles': 'El ID de partida es obligatorio'
            })
        
        if not descripcion_concepto:
            return JsonResponse({
                'exito': False, 
                'tipo_aviso': 'error', 
                'detalles': 'La descripción es obligatoria'
            })
        
        # Check for duplicates
        if id_partida != 'NA':
            if ConceptoMaestro.objects.filter(
                partida_extraordinaria=id_partida, 
                activo=True
            ).exists():
                return JsonResponse({
                    'exito': False, 
                    'tipo_aviso': 'advertencia', 
                    'detalles': 'Ya existe un concepto extraordinario con esta clave.'
                })
        
        try:
            unidad_medida = UnidadMedida.objects.get(id=unidad_medida_id)
            tipo_partida = Tipo.objects.get(id=tipo_partida_id)
        except (UnidadMedida.DoesNotExist, Tipo.DoesNotExist):
            return JsonResponse({
                'exito': False, 
                'tipo_aviso': 'advertencia', 
                'detalles': 'Unidad de medida o Tipo de partida no válidos.'
            })

        try:
            precio_mn = Decimal(precio_unitario_mn) if precio_unitario_mn else Decimal('0')
            precio_usd = Decimal(precio_unitario_usd) if precio_unitario_usd else Decimal('0')
            cantidad = Decimal(cantidad_post) if cantidad_post else Decimal('0')
        except (InvalidOperation, ValueError):
            return JsonResponse({
                'exito': False, 
                'tipo_aviso': 'error', 
                'detalles': 'Valores numéricos inválidos.'
            })

        concepto = ConceptoMaestro.objects.create(
            partida_extraordinaria=id_partida,
            descripcion=descripcion_concepto,
            id_tipo_partida=tipo_partida,
            unidad_medida=unidad_medida,
            precio_unitario_mn=precio_mn,
            precio_unitario_usd=precio_usd,
            cantidad=cantidad,
            comentario=comentario,
            pte_creacion=pte_origen,
            ot_creacion=ot_origen,
            estatus='EN ELABORACION',
            activo=True
        )
        
        return JsonResponse({
            'exito': True,
            'tipo_aviso': 'exito',
            'detalles': 'Concepto extraordinario creado correctamente',
            'id': concepto.id
        })
        
    except Exception as e:
        return JsonResponse({
            'exito': False,
            'tipo_aviso': 'error',
            'detalles': f'Error al crear concepto: {str(e)}'
        })
Type ID 7: Extraordinary concepts (PUEs) are identified by id_tipo_partida_id=7.

Converting PUE to Ordinary Concept

A key workflow allows converting approved extraordinary concepts to ordinary contracted items:
operaciones/views/catalogos.py
@require_http_methods(["POST"])
def convertir_pue_a_ordinario(request):
    """
    Toma un concepto extraordinario, le asigna Partida Ordinaria y Anexo,
    y lo convierte a Ordinario.
    """
    try:
        pue_id = request.POST.get('id_pue')
        nueva_partida = request.POST.get('nueva_partida')
        clave_anexo = request.POST.get('nuevo_anexo')
        precio_mn = request.POST.get('precio_mn')
        precio_usd = request.POST.get('precio_usd')
        
        if not pue_id or not nueva_partida or not clave_anexo:
            return JsonResponse({
                'exito': False, 
                'tipo_aviso': 'advertencia', 
                'detalles': 'Faltan datos obligatorios (Partida u Anexo).'
            })

        anexo_maestro_id = 1
        
        try:
            anexo_maestro = AnexoContrato.objects.get(id=anexo_maestro_id)
            sub_anexo, created = SubAnexo.objects.get_or_create(
                anexo_maestro=anexo_maestro,
                clave_anexo=clave_anexo.strip().upper(),
                defaults={
                    'descripcion': 'Generado por conversión', 
                    'activo': True
                }
            )
        except Exception as e:
            return JsonResponse({
                'exito': False, 
                'tipo_aviso': 'error', 
                'detalles': f'Error con el Anexo: {str(e)}'
            })

        # Check for duplicate ordinary item
        if ConceptoMaestro.objects.filter(
            sub_anexo=sub_anexo, 
            partida_ordinaria=nueva_partida
        ).exclude(id=pue_id).exists():
            return JsonResponse({
                'exito': False, 
                'tipo_aviso': 'advertencia', 
                'detalles': f'La partida {nueva_partida} ya existe en el anexo {clave_anexo}.'
            })

        concepto = ConceptoMaestro.objects.get(id=pue_id)
        
        # Convert to ordinary
        concepto.id_tipo_partida_id = 6  # Change to ordinary type
        concepto.partida_ordinaria = nueva_partida
        concepto.sub_anexo = sub_anexo
        
        if precio_mn:
            concepto.precio_unitario_mn = Decimal(precio_mn)
        if precio_usd:
            concepto.precio_unitario_usd = Decimal(precio_usd)
            
        concepto.estatus = "AUTORIZADO"
        concepto.comentario = f"{concepto.comentario or ''} | Convertido de PUE ({concepto.partida_extraordinaria})".strip()
        concepto.save()

        return JsonResponse({'exito': True})

    except ConceptoMaestro.DoesNotExist:
        return JsonResponse({
            'exito': False, 
            'tipo_aviso': 'error', 
            'detalles': 'El concepto PUE no existe.'
        })
    except Exception as e:
        return JsonResponse({
            'exito': False, 
            'tipo_aviso': 'error', 
            'detalles': str(e)
        })

Units of Measure API

Listing Units

operaciones/views/catalogos.py
def datatable_unidad_medida(request):
    draw = int(request.GET.get('draw', 1))
    start = int(request.GET.get('start', 0))
    length = int(request.GET.get('length', 10))
    search_value = request.GET.get('filtro', '')
    
    tipos = UnidadMedida.objects.filter(activo=1).annotate(
        estado_texto=Case(
            When(activo=True, then=Value('Activo')),
            When(activo=False, then=Value('Inactivo')),
            default=Value('Desconocido'),
            output_field=CharField()
        ),
    )
    
    if search_value:
        tipos = tipos.filter(
            Q(descripcion__icontains=search_value) |
            Q(activo__icontains=search_value)
        )
    
    total_records = tipos.count()
    tipos = tipos[start:start + length]
    
    data = []
    for tipo in tipos:
        data.append({
            'id': tipo.id,
            'descripcion': tipo.descripcion,
            'activo': tipo.estado_texto,
            'activo_bool': tipo.activo,
            'clave': tipo.clave,
        })
    
    return JsonResponse({
        'draw': draw,
        'recordsTotal': total_records,
        'recordsFiltered': total_records,
        'data': data
    })

Getting All Active Units

operaciones/views/catalogos.py
@require_http_methods(["GET"])
def obtener_unidad_medida(request):
    """Obtener una unidad específica o todas las unidades de medida"""
    try:
        unidad_id = request.GET.get('id')
        
        if unidad_id:
            unidad_medida = UnidadMedida.objects.get(id=unidad_id)
            return JsonResponse({
                'id': unidad_medida.id,
                'descripcion': unidad_medida.descripcion,
                'clave': unidad_medida.clave,
                'comentario': unidad_medida.comentario,
                'activo': unidad_medida.activo
            })
        else:
            unidades = UnidadMedida.objects.filter(activo=True)
            unidades_data = []
            
            for unidad in unidades:
                unidades_data.append({
                    'id': unidad.id,
                    'descripcion': unidad.descripcion,
                    'clave': unidad.clave,
                    'comentario': unidad.comentario,
                    'activo': unidad.activo
                })
            
            return JsonResponse(unidades_data, safe=False)
            
    except UnidadMedida.DoesNotExist:
        return JsonResponse({
            'tipo_aviso': 'error',
            'detalles': 'Unidad de medida no encontrada'
        }, status=404)

URL Configuration

operaciones/urls.py
# URLs for Products & Concepts
path('catalogos/producto/', catalogos.lista_producto, name='lista_producto'),
path('catalogos/producto/datatable_producto/', catalogos.datatable_producto, name='datatable_producto'),
path('catalogos/producto/crear/', catalogos.crear_producto, name='crear_producto'),
path('catalogos/producto/eliminar/', catalogos.eliminar_producto, name='eliminar_producto'),
path('catalogos/producto/obtener/', catalogos.obtener_producto, name='obtener_producto'),
path('catalogos/producto/editar/', catalogos.editar_producto, name='editar_producto'),

# URLs for Concepts (Ordinary/Extraordinary)
path('catalogos/conceptos/ordinarios/', catalogos.lista_conceptos_ordinarios, name='lista_conceptos_ordinarios'),
path('catalogos/conceptos/extraordinarios/', catalogos.lista_conceptos_extraordinarios, name='lista_conceptos_extraordinarios'),
path('catalogos/concepto/datatable_concepto/', catalogos.datatable_conceptos, name='datatable_conceptos'),
path('catalogos/concepto/pues_disponibles/', catalogos.datatable_pues_disponibles, name='pues_disponibles'),
path('catalogos/concepto/convertir_pue/', catalogos.convertir_pue_a_ordinario, name='convertir_pue'),

# URLs for Units of Measure
path('catalogos/unidad_medida/', catalogos.lista_unidad_medida, name='lista_unidad_medida'),
path('catalogos/datatable_unidad_medida/', catalogos.datatable_unidad_medida, name='datatable_unidad_medida'),
path('catalogos/unidad_medida/crear/', catalogos.crear_unidad_medida, name='crear_unidad_medida'),
path('catalogos/unidad_medida/eliminar/', catalogos.eliminar_unidad_medida, name='eliminar_unidad_medida'),
path('catalogos/unidad_medida/obtener/', catalogos.obtener_unidad_medida, name='obtener_unidad_medida'),
path('catalogos/unidad_medida/editar/', catalogos.editar_unidad_medida, name='editar_unidad_medida'),

Best Practices

Pricing Validation: Always validate decimal precision for pricing fields:
try:
    precio_mn = Decimal(precio_unitario_mn) if precio_unitario_mn else Decimal('0')
    precio_usd = Decimal(precio_unitario_usd) if precio_unitario_usd else Decimal('0')
except (InvalidOperation, ValueError):
    return JsonResponse({
        'exito': False, 
        'tipo_aviso': 'error', 
        'detalles': 'Valores numéricos inválidos.'
    })
Efficient Queries: Use select_related() for foreign key relationships:
conceptos = ConceptoMaestro.objects.select_related(
    'sub_anexo', 
    'unidad_medida', 
    'id_tipo_partida'
)

Catalogs Overview

Return to catalog management overview

Build docs developers (and LLMs) love