import requests

from datetime import time
from decimal import Decimal, InvalidOperation
from django.db.models import Q
from django.db import transaction
from rest_framework.response import Response

from rest_framework import status
from .models import *

def sendPOST(url, data=None, headers=None):
    try:
        response = requests.post(url, json=data, headers=headers)
        response.raise_for_status()  
        return response.json()
    except requests.HTTPError as http_err:
        
        try:
            error_content = response.json()  
        except ValueError:
            error_content = response.text  
        return {'error': True, 'status_code': response.status_code, 'content': error_content}
    except requests.RequestException as e:
        
        print(f"Error al hacer la solicitud: {e}")
        return {'error': True, 'content': str(e)}

        
def sendGET(url, data=None, headers=None):
    try:
        response = requests.get(url, json=data, headers=headers)
        response.raise_for_status()
        return response.json()
    except requests.HTTPError as http_err:

        try:
            error_content = response.json()
        except ValueError:
            error_content = response.text
        return {'error': True, 'status_code': response.status_code, 'content': error_content}
    except requests.RequestException as e:

        print(f"Error al hacer la solicitud: {e}")
        return {'error': True, 'content': str(e)}


def buscar_precios_por_nombre(nombre):
    if not nombre:
        return Precios.objects.all()

    palabras = nombre.split()
    q = Q()
    for palabra in palabras:
        q &= Q(nombre__icontains=palabra)
    
    return Precios.objects.filter(q)


def actualizar_stock_producto(
    *,
    producto,
    sucursal_origen_id,
    cantidad,
    motivo,
    operacion_tipo,
    cantidad_anterior: Decimal = None,
    sucursal_destino_id: int = None
):
    """
    Actualiza el stock del producto usando campos saldo0000X.
    Si motivo == '04', hace un traslado entre sucursales (origen → destino).

    :param producto: instancia de Producto
    :param sucursal_origen_id: ID de la sucursal origen (int)
    :param cantidad: cantidad nueva (Decimal)
    :param motivo: código de motivo ('01' venta, '02' ingreso, '04' traslado, etc.)
    :param operacion_tipo: 'create', 'update' o 'delete'
    :param cantidad_anterior: cantidad anterior (solo en update)
    :param sucursal_destino_id: ID de la sucursal destino (solo para motivo '04')
    """

    if motivo == "01" or motivo == "14":
        print("Sin efecto")
        return

    def _modificar(producto, sucursal_id, delta):
        campo = f"saldo{int(sucursal_id):05d}"  # 'saldo00001', 'saldo00002', …
        if not hasattr(producto, campo):
            raise AttributeError(f"{producto!r} no tiene el campo {campo}")
        nuevo = getattr(producto, campo) + delta
        setattr(producto, campo, nuevo)
        producto.save(update_fields=[campo])

    # Lógica de traslado
    if motivo == "04":
        print("el motivo es tranferencia entre almacenes")
        if not sucursal_destino_id:
            raise ValueError("Para traslados (motivo '04') necesitas pasar sucursal_destino_id")

        if operacion_tipo == 'create':
            _modificar(producto, sucursal_origen_id, -cantidad)
            _modificar(producto, sucursal_destino_id,  cantidad)

        elif operacion_tipo == 'delete':
            _modificar(producto, sucursal_origen_id,  cantidad)
            _modificar(producto, sucursal_destino_id, -cantidad)

        elif operacion_tipo == 'update':
            if cantidad_anterior is None:
                raise ValueError("Para update necesitas cantidad_anterior")
            diff = cantidad - cantidad_anterior
            _modificar(producto, sucursal_origen_id, -diff)
            _modificar(producto, sucursal_destino_id,  diff)

        else:
            raise ValueError("operacion_tipo debe ser 'create', 'update' o 'delete'")

    # Lógica estándar (ingreso / salida)
    else:
        signo = 1 if motivo == '02' else -1

        if operacion_tipo == 'create':
            delta = signo * cantidad

        elif operacion_tipo == 'delete':
            delta = -signo * cantidad

        elif operacion_tipo == 'update':
            if cantidad_anterior is None:
                raise ValueError("Para update necesitas cantidad_anterior")
            diff = cantidad - cantidad_anterior
            delta = signo * diff

        else:
            raise ValueError("operacion_tipo debe ser 'create', 'update' o 'delete'")

        _modificar(producto, sucursal_origen_id, delta)




def procesar_cambio_estado_guia(guia, estado_anterior):
    """
    Al actualizar la cabecera de guía (EMI <-> ANU), procesa el stock:
     - EMI -> ANU: revertir movimiento (operacion 'delete')
     - ANU -> EMI: volver a aplicar movimiento (operacion 'create')
     - Para motivos de venta ('01') no se hace nada.
    """
    estado_nuevo = guia.estado

    # Solo procesar transición EMI <-> ANU
    if estado_nuevo == estado_anterior or {estado_anterior, estado_nuevo} != {'EMI', 'ANU'}:
        return

    operacion = 'delete' if (estado_anterior == 'EMI' and estado_nuevo == 'ANU') else 'create'

    detalles = guia.detguia_set.all()  # Ajusta related_name si es distinto
    with transaction.atomic():
        for det in detalles:
            producto = det.idproduct
            motivo   = guia.motivo      # '01', '02', '04', etc.
            cantidad = det.cantidad     # Decimal

            # Ignorar ventas
            if motivo == '01' or motivo == "14":
                continue

            # Determinar sucursales solo si es traslado
            if motivo == '04':
                # Origen
                suc_or = guia.idsucursal.idsucursal
                # Destino
                suc_dest = guia.idsucursal_destino.idsucursal
            else:
                # Para ingresos/salidas estándar
                suc_or = guia.idsucursal.idsucursal
                suc_dest = None

            # Ajustar stock mediante servicio genérico
            actualizar_stock_producto(
                producto=producto,
                sucursal_origen_id=suc_or,
                cantidad=Decimal(cantidad),
                motivo=motivo,
                operacion_tipo=operacion,
                cantidad_anterior=None,
                sucursal_destino_id=suc_dest,
            )


def anular_factura_si_corresponde(factura, estado_anterior):
    if factura.estado == 'ANU' and estado_anterior in ('EMI', 'CAN'):
        detalles = factura.detfactu_set.all()
        for detalle in detalles:
            articulo = detalle.idproduct
            if articulo.tipo != 'SER':
                cantidad = detalle.cantidad
                sucursal_id = factura.idsucursal
                print(f"Datos {cantidad}  {sucursal_id}")
                if sucursal_id.idsucursal == 1:
                    articulo.saldo00001 += cantidad
                elif sucursal_id.idsucursal == 2:
                    articulo.saldo00002 += cantidad
                elif sucursal_id.idsucursal == 3:
                    articulo.saldo00003 += cantidad
                elif sucursal_id.idsucursal == 4:
                    articulo.saldo00004 += cantidad
                elif sucursal_id.idsucursal == 5:
                    articulo.saldo00005 += cantidad
                elif sucursal_id.idsucursal == 6:
                    articulo.saldo00006 += cantidad
                elif sucursal_id.idsucursal == 7:
                    articulo.saldo00007 += cantidad
                elif sucursal_id.idsucursal == 8:
                    articulo.saldo00008 += cantidad
                elif sucursal_id.idsucursal == 9:
                    articulo.saldo00009 += cantidad

                articulo.save()


def revertir_ajuste_si_corresponde(ajuste, estado_anterior):
    """
    Si un ajuste pasa a estado 'ANU' y antes estaba en 'EMI',
    se recorren sus detalles y se revierten las cantidades sobre los saldos.
    """
    # Sólo revertimos si el estado anterior era 'EMI'
    if ajuste.estado == 'ANU' and estado_anterior == 'EMI':
        detalles = ajuste.detajuste_set.all()
        motivos_entrada = {'05', '16', '17'}  # sólo estos son ingresos

        with transaction.atomic():
            for det in detalles:
                articulo = det.idproduct
                cantidad = det.cantidad
                suc_id = ajuste.idsucursal.idsucursal  # suponemos FK a Sucursal

                # Construimos el nombre del campo de saldo (e.g. 'saldo00001')
                campo_saldo = f"saldo{str(suc_id).zfill(5)}"
                saldo_actual = getattr(articulo, campo_saldo, None)
                if saldo_actual is None:
                    # Si no existe el atributo, saltamos este detalle
                    continue

                # Para motivos de entrada quitamos stock; para los demás, lo devolvemos
                if ajuste.tipo_ope in motivos_entrada:
                    nuevo_saldo = saldo_actual - cantidad
                else:
                    nuevo_saldo = saldo_actual + cantidad

                # Guardamos sólo el campo modificado
                setattr(articulo, campo_saldo, nuevo_saldo)
                articulo.save(update_fields=[campo_saldo])

class KardexGeneralService:
    
    @staticmethod
    def to_decimal_safe(value):
        """Convierte valores a Decimal de forma segura"""
        if value is None or value == '':
            return Decimal('0')
        if isinstance(value, Decimal):
            return value
        try:
            return Decimal(str(value))
        except (InvalidOperation, TypeError, ValueError):
            return Decimal('0')

    @staticmethod
    def calcular_kardex_valorado_por_articulo(idarticulo, fecha_inicio, fecha_fin, idsucursal):
        """
        Calcula el kardex valorado para un artículo específico
        ACTUALIZADO: Valoriza órdenes de producción antes de calcular kardex
        """
        # Mapa de prioridades (mismo que en KardexAPIPreVal)
        tipo_prioridad_map = {
            'ajuste': 0,
            'compra': 1,
            'guia_destino': 2,
            'op_ingreso': 2,
            'nota_credito': 3,
            'guia_origen': 4,
            'op_salida': 4,
            'venta': 5
        }

        try:
            # **PASO 1: VALORIZAR ÓRDENES DE PRODUCCIÓN DEL PERIODO**
            # Esto debe hacerse ANTES de obtener los movimientos físicos
            KardexGeneralService._valorizar_ordenes_produccion_periodo(
                idarticulo=idarticulo,
                fecha_inicio=fecha_inicio,
                fecha_fin=fecha_fin,
                idsucursal=idsucursal,
                prioridad_map=tipo_prioridad_map
            )
            
            # **PASO 2: Obtener movimientos físicos usando la lógica existente**
            from .views import KardexAPIPreVal
            kardex_fisico = KardexAPIPreVal()
            
            # Crear request simulada para el artículo específico
            class MockRequest:
                def __init__(self, data):
                    self.data = data
            
            request_data = {
                'id': idarticulo,
                'fecini': fecha_inicio,
                'fecfin': fecha_fin,
                'idsucursal': idsucursal
            }
            
            # Obtener datos físicos del kardex
            response = kardex_fisico.post(MockRequest(request_data))
            if response.status_code != 200:
                return []
                
            datos_fisicos = response.data
            
            # **PASO 3: Valorizar los movimientos**
            return KardexGeneralService._valorizar_movimientos(
                datos_fisicos, idarticulo, tipo_prioridad_map, idsucursal=idsucursal, fecha_inicio=fecha_inicio
            )
            
        except Exception as e:
            print(f"Error calculando kardex para artículo {idarticulo}: {str(e)}")
            import traceback
            traceback.print_exc()
            return []

    # En KardexGeneralService
    @staticmethod
    def calcular_kardex_valorado_por_articulo1(idarticulo, fecha_inicio, fecha_fin, idsucursal):
        """
        Reutiliza la lógica de KardexValoradoAPIView para calcular el kardex valorado
        """
        from django.test import RequestFactory
        from .views import KardexValoradoAPIView
        
        # Simular request
        factory = RequestFactory()
        request = factory.post('/', {
            'id': idarticulo,
            'fecini': fecha_inicio,
            'fecfin': fecha_fin,
            'idsucursal': idsucursal
        })
        
        view = KardexValoradoAPIView()
        response = view.post(request)
        
        if response.status_code == 200:
            return response.data
        return []

    @staticmethod
    def _valorizar_ordenes_produccion_periodo(idarticulo, fecha_inicio, fecha_fin, 
                                               idsucursal, prioridad_map):
        """
        Valoriza todas las órdenes de producción del periodo que afectan al artículo
        NUEVO: Integrado desde KardexAPIPreVal
        """
        from .models import (DetOrdenProdSalida, DetOrdenProdIngreso, 
                            OrdenProduccion, Articulos)
        from datetime import datetime
        from django.db import transaction
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        # Convertir fechas si son strings
        if isinstance(fecha_inicio, str):
            fecha_inicio = datetime.strptime(fecha_inicio, '%Y-%m-%d').date()
        if isinstance(fecha_fin, str):
            fecha_fin = datetime.strptime(fecha_fin, '%Y-%m-%d').date()

        # Identificar órdenes que afectan al artículo en el periodo
        ordenes_con_salidas = DetOrdenProdSalida.objects.filter(
            idproduct=idarticulo,
            idorden__fecha__range=[fecha_inicio, fecha_fin],
            idorden__idsucursal=idsucursal
        ).values_list('idorden', flat=True).distinct()

        ordenes_con_ingresos = DetOrdenProdIngreso.objects.filter(
            idproduct=idarticulo,
            fecha__range=[fecha_inicio, fecha_fin],
            idorden__idsucursal=idsucursal
        ).values_list('idorden', flat=True).distinct()

        # Unir ambas listas
        ordenes_ids = set(ordenes_con_salidas) | set(ordenes_con_ingresos)

        if not ordenes_ids:
            return  # No hay órdenes que valorizar

        # Obtener órdenes completas ordenadas por fecha
        ordenes = OrdenProduccion.objects.filter(
            idorden__in=ordenes_ids
        ).exclude(estado='ANU').order_by('fecha', 'hora')

        print(f"    Valorizando {len(ordenes)} órdenes de producción para artículo {idarticulo}")

        # Valorizar cada orden
        for orden in ordenes:
            try:
                KardexGeneralService._valorizar_orden_produccion(
                    orden, idsucursal, prioridad_map
                )
            except Exception as e:
                print(f"       Error valorizando orden {orden.idorden}: {str(e)}")
                continue

    @staticmethod
    @transaction.atomic
    def _valorizar_orden_produccion(orden, idsucursal, prioridad_map):
        """
        Valoriza una orden de producción específica
        NOTA: Para órdenes GLOBALES, se pasa orden.idsucursal_id
        """
        from .models import DetOrdenProdSalida, DetOrdenProdIngreso
        from django.db import transaction
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        # 1. VALORIZAR MATERIAS PRIMAS (SALIDAS)
        materias_primas = DetOrdenProdSalida.objects.filter(
            idorden=orden
        ).select_related('idproduct')

        suma_total_mp = Decimal('0')

        for mp in materias_primas:
            # Para contexto GLOBAL, usar la sucursal de la orden
            sucursal_a_usar = idsucursal if idsucursal else orden.idsucursal_id
            
            # Obtener costo promedio a la fecha de la orden
            if sucursal_a_usar:
                # Contexto por sucursal
                costo_promedio = KardexGeneralService._obtener_costo_promedio_a_fecha(
                    mp.idproduct.idproduct,
                    orden.fecha,
                    sucursal_a_usar,
                    prioridad_map
                )
            else:
                # Contexto global
                costo_promedio = KardexGeneralService._obtener_costo_promedio_a_fecha_global(
                    mp.idproduct.idproduct,
                    orden.fecha,
                    prioridad_map
                )

            cantidad = to_decimal(mp.cantidad)
            costouni = costo_promedio
            total = costouni * cantidad

            # Actualizar en BD solo si el costo es válido
            if costouni >= 0:
                mp.costouni = costouni
                mp.total = total
                mp.save(update_fields=['costouni', 'total'])
                suma_total_mp += total

        # 2. VALORIZAR PRODUCTOS TERMINADOS (INGRESOS)
        productos_terminados = DetOrdenProdIngreso.objects.filter(
            idorden=orden
        ).select_related('idproduct')

        suma_produccion_merma = Decimal('0')

        # Calcular suma total de producción + merma
        for pt in productos_terminados:
            produccion = to_decimal(pt.produccion)
            mermas = to_decimal(pt.mermas)
            suma_produccion_merma += (produccion + mermas)

        # Calcular costo unitario del producto terminado
        if suma_produccion_merma > 0 and suma_total_mp > 0:
            costouni_pt = (suma_total_mp / suma_produccion_merma).quantize(Decimal('0.00001'))
        else:
            costouni_pt = Decimal('0')

        # Aplicar a cada producto terminado
        for pt in productos_terminados:
            produccion = to_decimal(pt.produccion)
            mermas = to_decimal(pt.mermas)
            total_pt = costouni_pt * (produccion + mermas)

            # Actualizar en BD
            pt.costounip = costouni_pt
            pt.total = total_pt
            pt.save(update_fields=['costounip', 'total'])

    @staticmethod
    def _obtener_costo_promedio_a_fecha(idarticulo, fecha_limite, idsucursal, prioridad_map):
        """
        Obtiene el costo promedio de un artículo hasta una fecha específica
        CRÍTICO: No incluye movimientos de OP para evitar recursión infinita
        ACTUALIZADO: Preserva costo promedio cuando inventario = 0
        """

        from datetime import datetime, time
        from django.db.models import Q
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        if isinstance(fecha_limite, str):
            fecha_limite = datetime.strptime(fecha_limite, '%Y-%m-%d').date()

        movimientos = []

        # COMPRAS (sin OP)
        compras = Detcompra.objects.filter(
            idproduct=idarticulo,
            idcompra__fecemi__lte=fecha_limite,
            idcompra__idsucursal=idsucursal
        ).exclude(idcompra__estado='ANU')

        for c in compras:
            cantidad = to_decimal(c.cantidad)
            precio = to_decimal(c.precio)
            flete = to_decimal(c.flete)
            otros = to_decimal(c.otros)
            tcambio = to_decimal(getattr(c.idcompra, 'tcambio', 1))
            
            flete_unitario = (flete / cantidad) if cantidad != 0 else Decimal('0')
            otros_unitario = (otros / cantidad) if cantidad != 0 else Decimal('0')
            # precompra = (precio + flete_unitario) * tcambio
            # Sumar todos los conceptos
            precompra = (precio + flete_unitario + otros_unitario) * tcambio  # ← MODIFICADO
            tipo_ope = getattr( c.idcompra, 'tipo_ope', None)
            movimientos.append({
                'fecha': c.idcompra.fecemi,
                'hora': getattr(c.idcompra, 'hora', time(0,0)),
                'tipo': 'compra',
                'tipo_ope': c.idcompra.tipo_ope,
                'cantidad': cantidad,
                'precio_unitario': precompra,
                'valor_total': precompra * cantidad,
                'prioridad': prioridad_map['compra']
            })

        # VENTAS (sin OP)
        ventas = Detfactu.objects.filter(
            idproduct=idarticulo,
            idfactura__fecha__lt=fecha_limite,
            idfactura__idsucursal=idsucursal
        ).exclude(
            Q(idfactura__estado='ANU') |
            Q(idfactura__documen='NCR')
        )

        for v in ventas:
            cantidad = to_decimal(v.cantidad)
            tipo_ope = getattr( v.idfactura, 'tipo_ope', None)
            movimientos.append({
                'fecha': v.idfactura.fecha,
                'hora': getattr(v.idfactura, 'hora', time(0,0)),
                'tipo': 'venta',
                'tipo_ope': v.idfactura.tipo_ope,
                'cantidad': cantidad,
                'precio_unitario': Decimal('0'),
                'valor_total': None,
                'prioridad': prioridad_map['venta']
            })

        # NOTAS DE CRÉDITO (sin OP)
        ncr = Detfactu.objects.filter(
            idproduct=idarticulo,
            idfactura__fecha__lt=fecha_limite,
            idfactura__idsucursal=idsucursal,
            idfactura__documen='NCR'
        ).exclude(
            Q(idfactura__motivo__in=['03', '04', '05', '08', '09', '13']) |
            Q(idfactura__estado='ANU')
        )

        for nc in ncr:
            cantidad = to_decimal(nc.cantidad)
            tipo_ope = getattr(nc.idfactura, 'tipo_ope', None)
            movimientos.append({
                'fecha': nc.idfactura.fecha,
                'hora': getattr(nc.idfactura, 'hora', time(0,0)),
                'tipo': 'nota_credito',
                'tipo_ope': nc.idfactura.tipo_ope,
                'cantidad': cantidad,
                'precio_unitario': Decimal('0'),
                'valor_total': None,
                'prioridad': prioridad_map['nota_credito']
            })

        # AJUSTES (sin OP)
        ajustes = Detajuste.objects.filter(
            idproduct=idarticulo,
            idajuste__fecha__lt=fecha_limite,
            idajuste__idsucursal=idsucursal
        ).exclude(idajuste__estado='ANU')

        for aj in ajustes:
            cantidad = to_decimal(aj.cantidad)
            valunit = to_decimal(aj.valunit)
            tipo_ope = getattr(aj.idajuste, 'tipo_ope', None)
            
            # ✅ CÓDIGO CORREGIDO
            if tipo_ope in ['02','05','16']:
                movimientos.append({
                    'fecha': aj.idajuste.fecha,
                    'hora': getattr(aj.idajuste, 'hora', time(0,0)),
                    'tipo': 'ajuste_entrada',
                    'tipo_ope': aj.idajuste.tipo_ope,
                    'cantidad': abs(cantidad),
                    'precio_unitario': valunit,
                    'valor_total': valunit * abs(cantidad) if valunit > 0 else None,
                    'prioridad': prioridad_map['ajuste']
                    
                })
            elif tipo_ope == '17':
                # Conversiones: determinar por el signo
                if cantidad >= 0:
                    movimientos.append({
                        'fecha': aj.idajuste.fecha,
                        'hora': getattr(aj.idajuste, 'hora', time(0,0)),
                        'tipo': 'ajuste_entrada',
                        'tipo_ope': aj.idajuste.tipo_ope,
                        'cantidad': abs(cantidad),
                        'precio_unitario': valunit,
                        'valor_total': valunit * abs(cantidad) if valunit > 0 else None,
                        'prioridad': prioridad_map['ajuste']
                    })
                else:
                    movimientos.append({
                        'fecha': aj.idajuste.fecha,
                        'hora': getattr(aj.idajuste, 'hora', time(0,0)),
                        'tipo': 'ajuste_salida',
                        'tipo_ope': aj.idajuste.tipo_ope,
                        'cantidad': abs(cantidad),
                        'precio_unitario': Decimal('0'),
                        'valor_total': None,
                        'prioridad': prioridad_map['ajuste']
                    })
            else:
                movimientos.append({
                    'fecha': aj.idajuste.fecha,
                    'hora': getattr(aj.idajuste, 'hora', time(0,0)),
                    'tipo': 'ajuste_salida',
                    'tipo_ope': aj.idajuste.tipo_ope,
                    'cantidad': abs(cantidad),
                    'precio_unitario': Decimal('0'),
                    'valor_total': None,
                    'prioridad': prioridad_map['ajuste']
                })

        # GUIAS ORIGEN (sin OP)
        guias = Detguia.objects.filter(
            idproduct=idarticulo,
            idguiar__fecha__lt=fecha_limite,
            idguiar__idsucursal=idsucursal
        ).exclude(
            Q(idguiar__motivo__in=['01','14']) |
            Q(idguiar__estado='ANU')
        )

        for g in guias:
            cantidad = to_decimal(g.cantidad)
            tipo_ope = getattr(g.idguiar, 'tipo_ope', None)
            movimientos.append({
                'fecha': g.idguiar.fecha,
                'hora': getattr(g.idguiar, 'hora', time(0,0)),
                'tipo': 'guia_origen',
                'tipo_ope': g.idguiar.tipo_ope,
                'cantidad': cantidad,
                'precio_unitario': Decimal('0'),
                'valor_total': None,
                'prioridad': prioridad_map['guia_origen']
            })

        # GUIAS DESTINO (sin OP)
        guias_dest = Detguia.objects.filter(
            idproduct=idarticulo,
            idguiar__fecha__lt=fecha_limite,
            idguiar__idsucursal_destino=idsucursal
        ).exclude(idguiar__estado='ANU')

        for g in guias_dest:
            cantidad = to_decimal(g.cantidad)
            costo_guia = to_decimal(g.costo) if g.costo else Decimal('0')
            tipo_ope = getattr(g.idguiar, 'tipo_ope', None)
            movimientos.append({
                'fecha': g.idguiar.fecha,
                'hora': getattr(g.idguiar, 'hora', time(0,0)),
                'tipo': 'guia_destino',
                'tipo_ope': g.idguiar.tipo_ope,
                'cantidad': cantidad,
                'precio_unitario': costo_guia,
                'valor_total': costo_guia * cantidad if costo_guia > 0 else None,
                'prioridad': prioridad_map['guia_destino']
            })

        # INCLUIR OP ANTERIORES (solo las que ya tienen costo calculado)
        op_ingresos = DetOrdenProdIngreso.objects.filter(
            idproduct=idarticulo,
            fecha__lt=fecha_limite,
            idorden__idsucursal=idsucursal
        ).select_related('idorden')

        for opi in op_ingresos:
            produccion = to_decimal(opi.produccion)
            costounip = to_decimal(opi.costounip)
            
            movimientos.append({
                'fecha': opi.fecha,
                'hora': getattr(opi.idorden, 'hora', time(0,0)),
                'tipo': 'op_ingreso',
                'tipo_ope': 'op_ingreso',
                'cantidad': produccion,
                'precio_unitario': costounip,
                'valor_total': costounip * produccion if costounip > 0 else None,
                'prioridad': prioridad_map['op_ingreso']
            })

        # OP SALIDAS (sin valorizar, solo físicas)
        op_salidas = DetOrdenProdSalida.objects.filter(
            idproduct=idarticulo,
            idorden__fecha__lt=fecha_limite,
            idorden__idsucursal=idsucursal
        )

        for ops in op_salidas:
            cantidad = to_decimal(ops.cantidad)
            movimientos.append({
                'fecha': ops.idorden.fecha,
                'hora': getattr(ops.idorden, 'hora', time(0,0)),
                'tipo': 'op_salida',
                'tipo_ope': 'op_salida',
                'cantidad': cantidad,
                'precio_unitario': Decimal('0'),
                'valor_total': None,
                'prioridad': prioridad_map['op_salida']
            })

        # ORDENAR
        movimientos.sort(key=lambda x: (
            x['fecha'], 
            x['hora'] or time(0,0), 
            x['prioridad']
        ))

        # Calcular costo promedio progresivo
        inventario_fisico = Decimal('0')
        inventario_valorado = Decimal('0')
        costo_promedio = Decimal('0')

        tipos_entrada = {'compra', 'ajuste_entrada', 'nota_credito', 'guia_destino', 'op_ingreso'}
        tipos_salida = {'venta', 'ajuste_salida', 'guia_origen', 'op_salida'}

        for mov in movimientos:
            tipo = mov['tipo']
            cantidad = abs(mov['cantidad'])
            precio_unitario = mov['precio_unitario']
            valor_total = mov['valor_total']

            if tipo == 'compra':
                inventario_fisico += cantidad
                inventario_valorado += valor_total

            elif tipo == 'ajuste_entrada':
                if valor_total is not None and valor_total > 0:
                    inventario_fisico += cantidad
                    inventario_valorado += valor_total
                else:
                    inventario_fisico += cantidad
                    inventario_valorado += (costo_promedio * cantidad)

            elif tipo in tipos_salida:
                inventario_fisico -= cantidad
                inventario_valorado -= (costo_promedio * cantidad)

            elif tipo in {'nota_credito', 'guia_destino', 'op_ingreso'}:
                inventario_fisico += cantidad
                # CORRECCIÓN: Para guia_destino, si tiene valor_total usar ese, sino usar costo promedio
                if valor_total is not None and valor_total > 0:
                    inventario_valorado += valor_total
                else:
                    inventario_valorado += (costo_promedio * cantidad)

            # Actualizar costo promedio
            inventario_valorado = inventario_valorado.quantize(Decimal('0.00001'))
            
            # Limpiar micro-diferencias
            if abs(inventario_valorado) < Decimal('0.01'):
                inventario_valorado = Decimal('0')
            
            if inventario_fisico > 0:
                costo_promedio = (inventario_valorado / inventario_fisico).quantize(Decimal('0.00001'))
            # Si inventario_fisico == 0, mantener costo_promedio

        return costo_promedio

    @staticmethod
    def _valorizar_movimientos1(datos_fisicos, idarticulo, tipo_prioridad_map):
        """
        Valoriza los movimientos físicos aplicando costos promedio
        ACTUALIZADO: Maneja correctamente OP valorizadas y preserva costo promedio
        """
        from .models import DetOrdenProdIngreso, Detguia
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        # Buscar saldo previo
        saldo_prev_item = next((i for i in datos_fisicos if i.get('referencia') == 'Saldo Previo'), None)
        
        # Inicializar acumuladores
        acum_entrada_fisica = Decimal('0')
        acum_salida_fisica = Decimal('0')
        acum_entrada_valorada = Decimal('0')
        acum_salida_valorada = Decimal('0')
        costopro = Decimal('0')

        if saldo_prev_item:
            print(f"DEBUG saldo_prev: {dict(saldo_prev_item)}")
            saldo_fisico_inicial = to_decimal(saldo_prev_item.get('saldoinventario'))
            saldo_valorado_inicial = to_decimal(saldo_prev_item.get('saldo_valorado'))
            costopro_inicial = to_decimal(saldo_prev_item.get('costopromedio'))

            acum_entrada_fisica = saldo_fisico_inicial
            acum_entrada_valorada = saldo_valorado_inicial
            costopro = costopro_inicial

        # Prefetch de costos de guías
        guias_cost_map = {}
        try:
            guias_costos = Detguia.objects.filter(costo__gt=0).values('idguiar', 'idproduct', 'costo')
            for g in guias_costos:
                key = (int(g['idguiar']), int(g['idproduct']))
                guias_cost_map[key] = to_decimal(g['costo'])
        except:
            pass

        # Prefetch de costos de OP
        op_cost_map = {}
        try:
            op_costos = DetOrdenProdIngreso.objects.filter(costounip__gt=0).values('idorden_id', 'idproduct_id', 'costounip')
            for op in op_costos:
                key = (op['idorden_id'], op['idproduct_id'])
                op_cost_map[key] = to_decimal(op['costounip'])
        except:
            pass

        resultados = []
        
        for item in datos_fisicos:
            if item.get('referencia') == 'Saldo Previo':
                item['precompra'] = float(costopro)
                item['entrada_valorada'] = 0.0
                item['salida_valorada'] = 0.0
                item['saldo_valorado'] = float(acum_entrada_valorada - acum_salida_valorada)
                item['costopromedio'] = float(costopro)
                resultados.append(item)
                continue

            # Obtener cantidades
            try:
                cnt = to_decimal(item.get('cantidad') or 0)
            except (TypeError, ValueError):
                cnt = Decimal('0')

            tipomov = (item.get('tipomovimiento') or '').upper()
            entrada_d = abs(cnt) if tipomov == 'ENTRADA' else Decimal('0')
            salida_d = abs(cnt) if tipomov == 'SALIDA' else Decimal('0')

            # Obtener precompra del item
            raw_pre = item.get('precompra', None)
            precompra = None if raw_pre in (None, '') else to_decimal(raw_pre)

            # Variables para cálculo
            tipo = item.get('tipo')
            idguiar = item.get('idguiar')
            
            # Obtener costo de guía si existe
            costo_guia = Decimal('0')
            if idguiar:
                try:
                    idg_tmp = int(idguiar)
                    idart_tmp = int(idarticulo)
                    costo_guia = guias_cost_map.get((idg_tmp, idart_tmp), Decimal('0'))
                except (TypeError, ValueError):
                    costo_guia = Decimal('0')

            # Obtener costo de OP si existe
            costo_op = Decimal('0')
            if tipo == 'op_ingreso' and item.get('numero'):
                try:
                    idorden_int = int(item.get('numero'))
                    idart_int = int(idarticulo)
                    costo_op = op_cost_map.get((idorden_int, idart_int), Decimal('0'))
                except (TypeError, ValueError):
                    costo_op = Decimal('0')

            # Costo para valorización (costo promedio antes de la operación)
            costo_para_valorizacion = costopro
            
            # Variables para resultado
            entval = Decimal('0')
            salval = Decimal('0')
            unit_price = costopro  # Precio unitario por defecto

            # Cantidad antes de la operación
            prev_qty = acum_entrada_fisica - acum_salida_fisica

            # ---- LÓGICA DE VALORIZACIÓN SEGÚN TIPO ----
            if tipo == 'compra':
                # Usar precompra si existe, sino costopro
                if precompra is not None:
                    unit_price = precompra
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)  # Mostrar precio de compra

            elif tipo == 'ajuste_entrada':
                # Usar precompra si existe, sino costopro
                if precompra is not None:
                    unit_price = precompra
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)  # Mostrar precio de ajuste

            elif tipo == 'guia_destino':
                # Usar costo de guía si existe, sino costopro
                if costo_guia > 0:
                    unit_price = costo_guia
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo == 'op_ingreso':
                # Usar costo de OP si existe, sino costopro
                if costo_op > 0:
                    unit_price = costo_op
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo == 'nota_credito':
                # Notas de crédito usan costopro para valorización
                unit_price = costopro
                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
                
                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo in ('venta', 'ajuste_salida', 'guia_origen', 'op_salida'):
                # SALIDAS: usar costo promedio vigente ANTES de la operación
                salval = (costo_para_valorizacion * salida_d).quantize(Decimal('0.00001'))
                acum_salida_fisica += salida_d
                acum_salida_valorada += salval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if abs(sal_val) < Decimal('0.01'):
                    sal_val = Decimal('0')

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    new_costopro = costopro  # Mantener costo previo si no hay stock

                costopro = new_costopro
                
                # CORRECCIÓN IMPORTANTE: Mostrar el costo utilizado para la salida, no 0
                item['precompra'] = float(costo_para_valorizacion)
                item['salida_valorada'] = float(salval)

            else:
                # Fallback para tipos no reconocidos
                if entrada_d > 0:
                    unit_price = costopro
                    entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                    acum_entrada_fisica += entrada_d
                    acum_entrada_valorada += entval
                else:
                    salval = (costo_para_valorizacion * salida_d).quantize(Decimal('0.00001'))
                    acum_salida_fisica += salida_d
                    acum_salida_valorada += salval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
                
                if abs(sal_val) < Decimal('0.01'):
                    sal_val = Decimal('0')
                
                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            # Protección contra costo promedio negativo
            if costopro < 0:
                costopro = Decimal('0')
                item['costopromedio'] = float(costopro)

            # Asegurar campos mostrados
            if 'entrada_valorada' not in item:
                item['entrada_valorada'] = float(entval)
            if 'salida_valorada' not in item:
                item['salida_valorada'] = float(salval)
            
            # Calcular saldo valorado actual
            saldo_valorado_actual = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
            item['saldo_valorado'] = float(saldo_valorado_actual)
            item['costopromedio'] = float(costopro)

            resultados.append(item)

        return resultados
    

    @staticmethod
    def _valorizar_movimientos(datos_fisicos, idarticulo, tipo_prioridad_map, idsucursal, fecha_inicio=None):
        """
        Valoriza los movimientos físicos aplicando costos promedio v2
        ACTUALIZADO: Maneja correctamente OP valorizadas y preserva costo promedio
        """
        from .models import DetOrdenProdIngreso, Detguia
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        # Buscar saldo previo
        saldo_prev_item = next((i for i in datos_fisicos if i.get('referencia') == 'Saldo Previo'), None)
        
        # Inicializar acumuladores
        acum_entrada_fisica = Decimal('0')
        acum_salida_fisica = Decimal('0')
        acum_entrada_valorada = Decimal('0')
        acum_salida_valorada = Decimal('0')
        costopro = Decimal('0')

        if saldo_prev_item:
            saldo_fisico_inicial = to_decimal(saldo_prev_item.get('saldoinventario'))

            # ✅ Obtener la fecha del primer movimiento real para calcular costo previo
            fecha_primer_mov = None
            for item in datos_fisicos:
                if item.get('referencia') != 'Saldo Previo':
                    fecha_primer_mov = item.get('fecha')
                    break
            
            # ✅ Si no hay movimientos en el periodo, usar fecha_inicio como referencia
            fecha_para_costo = fecha_primer_mov or fecha_inicio

            # ✅ Recalcular costo promedio histórico correctamente
            if fecha_para_costo  and saldo_fisico_inicial > 0:
                try:
                    costopro = KardexGeneralService._obtener_costo_promedio_a_fecha(
                        idarticulo=idarticulo,
                        fecha_limite=fecha_para_costo,
                        idsucursal=idsucursal,
                        prioridad_map=tipo_prioridad_map
                    )
                    print(f"DEBUG costo_previo artículo {idarticulo}: {costopro}")
                except Exception as e:
                    print(f"ERROR obteniendo costo previo artículo {idarticulo}: {e}")
                    costopro = Decimal('0')

            saldo_valorado_inicial = costopro * saldo_fisico_inicial
            acum_entrada_fisica   = saldo_fisico_inicial
            acum_entrada_valorada = saldo_valorado_inicial

            # Actualizar el item saldo previo con valores correctos
            saldo_prev_item['costopromedio'] = float(costopro)
            saldo_prev_item['saldo_valorado'] = float(saldo_valorado_inicial)
        
        # Prefetch de costos de guías
        guias_cost_map = {}
        try:
            guias_costos = Detguia.objects.filter(costo__gt=0).values('idguiar', 'idproduct', 'costo')
            for g in guias_costos:
                key = (int(g['idguiar']), int(g['idproduct']))
                guias_cost_map[key] = to_decimal(g['costo'])
        except:
            pass

        # Prefetch de costos de OP
        op_cost_map = {}
        try:
            op_costos = DetOrdenProdIngreso.objects.filter(costounip__gt=0).values('idorden_id', 'idproduct_id', 'costounip')
            for op in op_costos:
                key = (op['idorden_id'], op['idproduct_id'])
                op_cost_map[key] = to_decimal(op['costounip'])
        except:
            pass

        resultados = []
        
        for item in datos_fisicos:
            if item.get('referencia') == 'Saldo Previo':
                item['precompra'] = float(costopro)
                item['entrada_valorada'] = 0.0
                item['salida_valorada'] = 0.0
                item['saldo_valorado'] = float(acum_entrada_valorada - acum_salida_valorada)
                item['costopromedio'] = float(costopro)
                resultados.append(item)
                continue

            # Obtener cantidades
            try:
                cnt = to_decimal(item.get('cantidad') or 0)
            except (TypeError, ValueError):
                cnt = Decimal('0')

            tipomov = (item.get('tipomovimiento') or '').upper()
            entrada_d = abs(cnt) if tipomov == 'ENTRADA' else Decimal('0')
            salida_d = abs(cnt) if tipomov == 'SALIDA' else Decimal('0')

            # Obtener precompra del item
            raw_pre = item.get('precompra', None)
            precompra = None if raw_pre in (None, '') else to_decimal(raw_pre)

            # Variables para cálculo
            tipo = item.get('tipo')
            idguiar = item.get('idguiar')
            
            # Obtener costo de guía si existe
            costo_guia = Decimal('0')
            if idguiar:
                try:
                    idg_tmp = int(idguiar)
                    idart_tmp = int(idarticulo)
                    costo_guia = guias_cost_map.get((idg_tmp, idart_tmp), Decimal('0'))
                except (TypeError, ValueError):
                    costo_guia = Decimal('0')

            # Obtener costo de OP si existe
            costo_op = Decimal('0')
            if tipo == 'op_ingreso' and item.get('numero'):
                try:
                    idorden_int = int(item.get('numero'))
                    idart_int = int(idarticulo)
                    costo_op = op_cost_map.get((idorden_int, idart_int), Decimal('0'))
                except (TypeError, ValueError):
                    costo_op = Decimal('0')

            # Costo para valorización (costo promedio antes de la operación)
            costo_para_valorizacion = costopro
            
            # Variables para resultado
            entval = Decimal('0')
            salval = Decimal('0')
            unit_price = costopro  # Precio unitario por defecto

            # Cantidad antes de la operación
            prev_qty = acum_entrada_fisica - acum_salida_fisica

            # ---- LÓGICA DE VALORIZACIÓN SEGÚN TIPO ----
            if tipo == 'compra':
                # Usar precompra si existe, sino costopro
                if precompra is not None:
                    unit_price = precompra
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)  # Mostrar precio de compra

            elif tipo == 'ajuste_entrada':
                # Usar precompra si existe, sino costopro
                if precompra is not None:
                    unit_price = precompra
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)  # Mostrar precio de ajuste

            elif tipo == 'guia_destino':
                # Usar costo de guía si existe, sino costopro
                if costo_guia > 0:
                    unit_price = costo_guia
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo == 'op_ingreso':
                # Usar costo de OP si existe, sino costopro
                if costo_op > 0:
                    unit_price = costo_op
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo == 'nota_credito':
                # Notas de crédito usan costopro para valorización
                unit_price = costopro
                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
                
                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo in ('venta', 'ajuste_salida', 'guia_origen', 'op_salida'):
                # SALIDAS: usar costo promedio vigente ANTES de la operación
                salval = (costo_para_valorizacion * salida_d).quantize(Decimal('0.00001'))
                acum_salida_fisica += salida_d
                acum_salida_valorada += salval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if abs(sal_val) < Decimal('0.01'):
                    sal_val = Decimal('0')

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    new_costopro = costopro  # Mantener costo previo si no hay stock

                costopro = new_costopro
                
                # CORRECCIÓN IMPORTANTE: Mostrar el costo utilizado para la salida, no 0
                item['precompra'] = float(costo_para_valorizacion)
                item['salida_valorada'] = float(salval)

            else:
                # Fallback para tipos no reconocidos
                if entrada_d > 0:
                    unit_price = costopro
                    entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                    acum_entrada_fisica += entrada_d
                    acum_entrada_valorada += entval
                else:
                    salval = (costo_para_valorizacion * salida_d).quantize(Decimal('0.00001'))
                    acum_salida_fisica += salida_d
                    acum_salida_valorada += salval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
                
                if abs(sal_val) < Decimal('0.01'):
                    sal_val = Decimal('0')
                
                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            # Protección contra costo promedio negativo
            if costopro < 0:
                costopro = Decimal('0')
                item['costopromedio'] = float(costopro)

            # Asegurar campos mostrados
            if 'entrada_valorada' not in item:
                item['entrada_valorada'] = float(entval)
            if 'salida_valorada' not in item:
                item['salida_valorada'] = float(salval)
            
            # Calcular saldo valorado actual
            saldo_valorado_actual = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
            item['saldo_valorado'] = float(saldo_valorado_actual)
            item['costopromedio'] = float(costopro)

            resultados.append(item)

        return resultados
    


    @staticmethod
    def _calcular_valorizacion(item, entrada_d, salida_d, precompra, costopro, guias_cost_map, idarticulo=None):
        """
        AGREGADO: Parámetro idarticulo para buscar costo correcto
        """
        to_decimal = KardexGeneralService.to_decimal_safe
        tipo = item.get('tipo')
        idguiar = item.get('idguiar')
        
        entval = Decimal('0')
        salval = Decimal('0')

        # Obtener costo de guía si existe (CON idproduct)
        costo_guia = Decimal('0')
        if idguiar and idarticulo:
            try:
                idg_tmp = int(idguiar)
                idart_tmp = int(idarticulo)
                costo_guia = guias_cost_map.get((idg_tmp, idart_tmp), Decimal('0'))
            except (TypeError, ValueError):
                costo_guia = Decimal('0')

        # Validar que costopro sea válido
        costopro_valido = costopro if costopro > 0 else Decimal('0')
        
        # Lógica de valorización por tipo
        if tipo == 'compra':
            entval = (precompra * entrada_d).quantize(Decimal('0.00001'))
            
        elif tipo == 'ajuste_entrada':
            if precompra is not None:
                entval = (precompra * entrada_d).quantize(Decimal('0.00001'))
            else:
                entval = (costopro_valido * entrada_d).quantize(Decimal('0.00001'))
                
        elif tipo == 'guia_destino':
            # Valorizar con costo de ORIGEN (costo_guia)
            if costo_guia > 0:
                entval = (costo_guia * entrada_d).quantize(Decimal('0.00001'))
            else:
                entval = (costopro_valido * entrada_d).quantize(Decimal('0.00001'))
        
        elif tipo == 'op_ingreso':
            if precompra > 0:
                entval = (precompra * entrada_d).quantize(Decimal('0.00001'))
            else:
                entval = (costopro_valido * entrada_d).quantize(Decimal('0.00001'))
                
        elif tipo in ['guia_origen', 'ajuste_salida', 'op_salida', 'venta']:
            salval = (costopro_valido * salida_d).quantize(Decimal('0.00001'))
                    
        elif tipo == 'nota_credito':
            entval = (costopro_valido * entrada_d).quantize(Decimal('0.00001'))
            
        else:
            entval = (costopro_valido * entrada_d).quantize(Decimal('0.00001'))
            salval = (costopro_valido * salida_d).quantize(Decimal('0.00001'))

        return entval, salval

    @staticmethod
    def _obtener_costos_guias():
        """
        Obtiene todos los costos de guías en un solo query para optimización
        """
        from .models import Detguia
        
        try:
            return {
                (g['idguiar'], g['idproduct']): KardexGeneralService.to_decimal_safe(g['costo'])
                for g in Detguia.objects.filter(costo__gt=0).values('idguiar', 'idproduct', 'costo')
            }
        except Exception as e:
            print(f"Error obteniendo costos de guías: {str(e)}")
            return {}
        
    @staticmethod
    def generar_kardex_general(fecha_inicio, fecha_fin, idsucursal, batch_size=100):
        """
        Genera kardex general para todos los artículos en lotes
        ACTUALIZADO: Incluye valorización de OP
        """
        from .models import Articulos
        
        print(f"   INICIANDO KARDEX GENERAL CON OP")
        print(f"   - Fecha: {fecha_inicio} a {fecha_fin}")
        print(f"   - Sucursal: {idsucursal}")

        resultados = []
        articulos = Articulos.objects.all().select_related('unidad')
        total_articulos = articulos.count()
        
        print(f" PROCESANDO {total_articulos} ARTÍCULOS...")

        for i in range(0, total_articulos, batch_size):
            batch = articulos[i:i + batch_size]
            print(f"    Lote {(i//batch_size)+1}/{(total_articulos+batch_size-1)//batch_size}")
            
            for articulo in batch:
                try:
                    movimientos = KardexGeneralService.calcular_kardex_valorado_por_articulo(
                        idarticulo=articulo.idproduct,
                        fecha_inicio=fecha_inicio,
                        fecha_fin=fecha_fin,
                        idsucursal=idsucursal
                    )
                    
                    if movimientos:
                        resultados.append({
                            'idproduct': articulo.idproduct,
                            'codigo': getattr(articulo, 'codigo', None),
                            'nombre': getattr(articulo, 'nombre', ''),
                            'tipo_exis': getattr(articulo, 'tipo_exis', ''),
                            'unidad': articulo.unidad.unidad if articulo.unidad else '',
                            'movimientos': movimientos,
                            'total_movimientos': len(movimientos)
                        })
                        
                except Exception as e:
                    print(f"Error en artículo {articulo.idproduct}: {str(e)}")

        print(f" COMPLETADO: {len(resultados)}/{total_articulos} con movimientos")
                        
        return resultados
    
    @staticmethod
    def calcular_kardex_valorado_por_articulo_global(idarticulo, fecha_inicio, fecha_fin):
        """
        Calcula el kardex valorado GLOBAL (todas las sucursales) para un artículo específico
        ACTUALIZADO: Valoriza órdenes de producción antes de calcular kardex
        """
        # Mapa de prioridades
        tipo_prioridad_map = {
            'ajuste': 0,
            'compra': 1,
            'guia_destino': 2,
            'op_ingreso': 2,
            'nota_credito': 3,
            'guia_origen': 4,
            'op_salida': 4,
            'venta': 5
        }

        try:
            # **PASO 1: VALORIZAR ÓRDENES DE PRODUCCIÓN GLOBALES DEL PERIODO**
            KardexGeneralService._valorizar_ordenes_produccion_periodo_global(
                idarticulo=idarticulo,
                fecha_inicio=fecha_inicio,
                fecha_fin=fecha_fin,
                prioridad_map=tipo_prioridad_map
            )
            
            # **PASO 2: Obtener movimientos físicos globales**
            from .views import KardexAPIPreValGlobal
            kardex_fisico = KardexAPIPreValGlobal()
            
            # Crear request simulada para el artículo específico (sin sucursal)
            class MockRequest:
                def __init__(self, data):
                    self.data = data
            
            request_data = {
                'id': idarticulo,
                'fecini': fecha_inicio,
                'fecfin': fecha_fin,
            }
            
            # Obtener datos físicos del kardex
            response = kardex_fisico.post(MockRequest(request_data))
            if response.status_code != 200:
                return []
                
            datos_fisicos = response.data
            
            # **PASO 3: Valorizar los movimientos**
            return KardexGeneralService._valorizar_movimientos_global(
                datos_fisicos, idarticulo, tipo_prioridad_map, fecha_inicio=fecha_inicio
            )
            
        except Exception as e:
            print(f"Error calculando kardex global para artículo {idarticulo}: {str(e)}")
            import traceback
            traceback.print_exc()
            return []

    @staticmethod
    def _valorizar_ordenes_produccion_periodo_global(idarticulo, fecha_inicio, fecha_fin, prioridad_map):
        """
        Valoriza todas las órdenes de producción GLOBALES (todas las sucursales) del periodo
        """
        from .models import (DetOrdenProdSalida, DetOrdenProdIngreso, 
                            OrdenProduccion, Articulos)
        from datetime import datetime
        from django.db import transaction
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        # Convertir fechas si son strings
        if isinstance(fecha_inicio, str):
            fecha_inicio = datetime.strptime(fecha_inicio, '%Y-%m-%d').date()
        if isinstance(fecha_fin, str):
            fecha_fin = datetime.strptime(fecha_fin, '%Y-%m-%d').date()

        # Identificar órdenes que afectan al artículo en el periodo (SIN FILTRO DE SUCURSAL)
        ordenes_con_salidas = DetOrdenProdSalida.objects.filter(
            idproduct=idarticulo,
            idorden__fecha__range=[fecha_inicio, fecha_fin]
        ).values_list('idorden', flat=True).distinct()

        ordenes_con_ingresos = DetOrdenProdIngreso.objects.filter(
            idproduct=idarticulo,
            fecha__range=[fecha_inicio, fecha_fin]
        ).values_list('idorden', flat=True).distinct()

        # Unir ambas listas
        ordenes_ids = set(ordenes_con_salidas) | set(ordenes_con_ingresos)

        if not ordenes_ids:
            return  # No hay órdenes que valorizar

        # Obtener órdenes completas ordenadas por fecha
        ordenes = OrdenProduccion.objects.filter(
            idorden__in=ordenes_ids
        ).exclude(estado='ANU').order_by('fecha', 'hora')

        print(f"    Valorizando {len(ordenes)} órdenes de producción GLOBALES para artículo {idarticulo}")

        # Valorizar cada orden
        for orden in ordenes:
            try:
                # Reutilizamos el método existente pasando la sucursal de cada orden
                KardexGeneralService._valorizar_orden_produccion(
                    orden, orden.idsucursal_id, prioridad_map
                )
            except Exception as e:
                print(f"       Error valorizando orden {orden.idorden}: {str(e)}")
                continue

    @staticmethod
    def _obtener_costo_promedio_a_fecha_global(idarticulo, fecha_limite, prioridad_map):
        """
        Obtiene el costo promedio GLOBAL de un artículo hasta una fecha específica
        CORREGIDO: Usa la misma lógica que el VIEW para consistencia
        """
        from .models import (Detcompra, Detfactu, Detajuste, Detguia,
                            DetOrdenProdIngreso, DetOrdenProdSalida)
        from datetime import datetime, time
        from django.db.models import Q
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        if isinstance(fecha_limite, str):
            fecha_limite = datetime.strptime(fecha_limite, '%Y-%m-%d').date()

        movimientos = []

        # COMPRAS (TODAS LAS SUCURSALES)
        compras = Detcompra.objects.filter(
            idproduct=idarticulo,
            idcompra__fecemi__lte=fecha_limite
        ).exclude(idcompra__estado='ANU').select_related('idcompra')

        for c in compras:
            cantidad = to_decimal(c.cantidad)
            precio = to_decimal(c.precio)
            flete = to_decimal(c.flete)
            otros = to_decimal(c.otros)
            tcambio = to_decimal(getattr(c.idcompra, 'tcambio', 1))
            
            flete_unitario = (flete / cantidad) if cantidad != 0 else Decimal('0')
            otros_unitario = (otros / cantidad) if cantidad != 0 else Decimal('0')
            # precompra = (precio + flete_unitario) * tcambio
            # Sumar todos los conceptos
            precompra = (precio + flete_unitario + otros_unitario) * tcambio  # ← MODIFICADO
            
            movimientos.append({
                'fecha': c.idcompra.fecemi,
                'hora': getattr(c.idcompra, 'hora', time(0,0)),
                'tipo': 'compra',
                'cantidad': cantidad,
                'precio_unitario': precompra,
                'valor_total': precompra * cantidad,
                'prioridad': prioridad_map['compra']
            })

        # VENTAS (TODAS LAS SUCURSALES)
        ventas = Detfactu.objects.filter(
            idproduct=idarticulo,
            idfactura__fecha__lt=fecha_limite
        ).exclude(
            Q(idfactura__estado='ANU') |
            Q(idfactura__documen='NCR')
        )

        for v in ventas:
            cantidad = to_decimal(v.cantidad)
            movimientos.append({
                'fecha': v.idfactura.fecha,
                'hora': getattr(v.idfactura, 'hora', time(0,0)),
                'tipo': 'venta',
                'cantidad': cantidad,
                'precio_unitario': Decimal('0'),
                'valor_total': None,
                'prioridad': prioridad_map['venta']
            })

        # NOTAS DE CRÉDITO (TODAS LAS SUCURSALES)
        ncr = Detfactu.objects.filter(
            idproduct=idarticulo,
            idfactura__fecha__lt=fecha_limite,
            idfactura__documen='NCR'
        ).exclude(
            Q(idfactura__motivo__in=['03', '04', '05', '08', '09', '13']) |
            Q(idfactura__estado='ANU')
        )

        for nc in ncr:
            cantidad = to_decimal(nc.cantidad)
            movimientos.append({
                'fecha': nc.idfactura.fecha,
                'hora': getattr(nc.idfactura, 'hora', time(0,0)),
                'tipo': 'nota_credito',
                'cantidad': cantidad,
                'precio_unitario': Decimal('0'),
                'valor_total': None,
                'prioridad': prioridad_map['nota_credito']
            })

        # AJUSTES (TODAS LAS SUCURSALES)
        ajustes = Detajuste.objects.filter(
            idproduct=idarticulo,
            idajuste__fecha__lt=fecha_limite
        ).exclude(idajuste__estado='ANU')

        for aj in ajustes:
            cantidad = to_decimal(aj.cantidad)
            valunit = to_decimal(aj.valunit)
            tipo_ope = getattr(aj.idajuste, 'tipo_ope', None)
            
            if tipo_ope in ['02','05', '16']:
                movimientos.append({
                    'fecha': aj.idajuste.fecha,
                    'hora': getattr(aj.idajuste, 'hora', time(0,0)),
                    'tipo': 'ajuste_entrada',
                    'cantidad': abs(cantidad),
                    'precio_unitario': valunit,
                    'valor_total': valunit * abs(cantidad) if valunit > 0 else None,
                    'prioridad': prioridad_map['ajuste']
                })
            elif tipo_ope == '17':
                if cantidad >= 0:
                    movimientos.append({
                        'fecha': aj.idajuste.fecha,
                        'hora': getattr(aj.idajuste, 'hora', time(0,0)),
                        'tipo': 'ajuste_entrada',
                        'cantidad': abs(cantidad),
                        'precio_unitario': valunit,
                        'valor_total': valunit * abs(cantidad) if valunit > 0 else None,
                        'prioridad': prioridad_map['ajuste']
                    })
                else:
                    movimientos.append({
                        'fecha': aj.idajuste.fecha,
                        'hora': getattr(aj.idajuste, 'hora', time(0,0)),
                        'tipo': 'ajuste_salida',
                        'cantidad': abs(cantidad),
                        'precio_unitario': Decimal('0'),
                        'valor_total': None,
                        'prioridad': prioridad_map['ajuste']
                    })
            else:
                movimientos.append({
                    'fecha': aj.idajuste.fecha,
                    'hora': getattr(aj.idajuste, 'hora', time(0,0)),
                    'tipo': 'ajuste_salida',
                    'cantidad': abs(cantidad),
                    'precio_unitario': Decimal('0'),
                    'valor_total': None,
                    'prioridad': prioridad_map['ajuste']
                })

        # GUÍAS - CORREGIDO: Usa la misma lógica que el VIEW
        todas_guias = Detguia.objects.filter(
            idproduct=idarticulo,
            idguiar__fecha__lt=fecha_limite
        ).exclude(idguiar__estado='ANU').select_related('idguiar')

        for g in todas_guias:
            cantidad = to_decimal(g.cantidad)
            guia = g.idguiar
            
            # MOVIMIENTO DE SALIDA (origen) - solo si el motivo NO es de ajuste
            if guia.motivo not in ['01', '14']:
                movimientos.append({
                    'fecha': guia.fecha,
                    'hora': getattr(guia, 'hora', time(0,0)),
                    'tipo': 'guia_origen',
                    'cantidad': cantidad,
                    'precio_unitario': Decimal('0'),
                    'valor_total': None,
                    'prioridad': prioridad_map['guia_origen']
                })
            
            # MOVIMIENTO DE ENTRADA (destino) - solo si es traslado (motivo='04')
            if guia.motivo == '04' and guia.idsucursal_destino:
                movimientos.append({
                    'fecha': guia.fecha,
                    'hora': getattr(guia, 'hora', time(0,0)),
                    'tipo': 'guia_destino',
                    'cantidad': cantidad,
                    'precio_unitario': Decimal('0'),
                    'valor_total': None,
                    'prioridad': prioridad_map['guia_destino']
                })

        # ORDEN PRODUCCIÓN INGRESOS (TODAS LAS SUCURSALES)
        op_ingresos = DetOrdenProdIngreso.objects.filter(
            idproduct=idarticulo,
            fecha__lt=fecha_limite
        ).select_related('idorden')

        for opi in op_ingresos:
            produccion = to_decimal(opi.produccion)
            costounip = to_decimal(opi.costounip)
            
            movimientos.append({
                'fecha': opi.fecha,
                'hora': getattr(opi.idorden, 'hora', time(0,0)),
                'tipo': 'op_ingreso',
                'cantidad': produccion,
                'precio_unitario': costounip,
                'valor_total': costounip * produccion if costounip > 0 else None,
                'prioridad': prioridad_map['op_ingreso']
            })

        # ORDEN PRODUCCIÓN SALIDAS (TODAS LAS SUCURSALES)
        op_salidas = DetOrdenProdSalida.objects.filter(
            idproduct=idarticulo,
            idorden__fecha__lt=fecha_limite
        )

        for ops in op_salidas:
            cantidad = to_decimal(ops.cantidad)
            movimientos.append({
                'fecha': ops.idorden.fecha,
                'hora': getattr(ops.idorden, 'hora', time(0,0)),
                'tipo': 'op_salida',
                'cantidad': cantidad,
                'precio_unitario': Decimal('0'),
                'valor_total': None,
                'prioridad': prioridad_map['op_salida']
            })

        # ORDENAR - MISMO ORDEN QUE EL VIEW
        movimientos.sort(key=lambda x: (
            x['fecha'], 
            x['hora'] or time(0,0), 
            x['prioridad']
        ))

        # Calcular costo promedio progresivo
        inventario_fisico = Decimal('0')
        inventario_valorado = Decimal('0')
        costo_promedio = Decimal('0')

        tipos_entrada = {'compra', 'ajuste_entrada', 'nota_credito', 'guia_destino', 'op_ingreso'}
        tipos_salida = {'venta', 'ajuste_salida', 'guia_origen', 'op_salida'}

        for mov in movimientos:
            tipo = mov['tipo']
            cantidad = abs(mov['cantidad'])
            precio_unitario = mov['precio_unitario']
            valor_total = mov['valor_total']

            if tipo == 'compra':
                inventario_fisico += cantidad
                inventario_valorado += valor_total

            elif tipo == 'ajuste_entrada':
                if valor_total is not None and valor_total > 0:
                    inventario_fisico += cantidad
                    inventario_valorado += valor_total
                else:
                    inventario_fisico += cantidad
                    inventario_valorado += (costo_promedio * cantidad)

            elif tipo in tipos_salida:
                inventario_fisico -= cantidad
                inventario_valorado -= (costo_promedio * cantidad)

            elif tipo in {'nota_credito', 'guia_destino', 'op_ingreso'}:
                inventario_fisico += cantidad
                # CORRECCIÓN: Para guia_destino, si tiene valor_total usar ese, sino usar costo promedio
                if valor_total is not None and valor_total > 0:
                    inventario_valorado += valor_total
                else:
                    inventario_valorado += (costo_promedio * cantidad)

            # Actualizar costo promedio
            inventario_valorado = inventario_valorado.quantize(Decimal('0.00001'))
            
            # Limpiar micro-diferencias
            if abs(inventario_valorado) < Decimal('0.01'):
                inventario_valorado = Decimal('0')
            
            if inventario_fisico > 0:
                costo_promedio = (inventario_valorado / inventario_fisico).quantize(Decimal('0.00001'))
            # Si inventario_fisico == 0, mantener costo_promedio

        return costo_promedio

    
    @staticmethod
    def _valorizar_movimientos_global1(datos_fisicos, idarticulo, tipo_prioridad_map):
        """
        Valoriza los movimientos físicos GLOBALES aplicando costos promedio
        CORREGIDO: Usa misma lógica que _valorizar_movimientos pero para contexto global
        """
        from .models import DetOrdenProdIngreso, Detguia
        from decimal import Decimal
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        # Buscar saldo previo
        saldo_prev_item = next((i for i in datos_fisicos if i.get('referencia') == 'Saldo Previo'), None)
        
        # Inicializar acumuladores (igual que en _valorizar_movimientos)
        acum_entrada_fisica = Decimal('0')
        acum_salida_fisica = Decimal('0')
        acum_entrada_valorada = Decimal('0')
        acum_salida_valorada = Decimal('0')
        costopro = Decimal('0')

        if saldo_prev_item:
            saldo_fisico_inicial = to_decimal(saldo_prev_item.get('saldoinventario'))
            saldo_valorado_inicial = to_decimal(saldo_prev_item.get('saldo_valorado'))
            costopro_inicial = to_decimal(saldo_prev_item.get('costopromedio'))

            acum_entrada_fisica = saldo_fisico_inicial
            acum_entrada_valorada = saldo_valorado_inicial
            costopro = costopro_inicial

        # Prefetch de costos de guías (GLOBAL)
        guias_cost_map = {}
        try:
            guias_costos = Detguia.objects.filter(costo__gt=0).values('idguiar', 'idproduct', 'costo')
            for g in guias_costos:
                key = (int(g['idguiar']), int(g['idproduct']))
                guias_cost_map[key] = to_decimal(g['costo'])
        except:
            pass

        # Prefetch de costos de OP (GLOBAL)
        op_cost_map = {}
        try:
            op_costos = DetOrdenProdIngreso.objects.filter(costounip__gt=0).values('idorden_id', 'idproduct_id', 'costounip')
            for op in op_costos:
                key = (op['idorden_id'], op['idproduct_id'])
                op_cost_map[key] = to_decimal(op['costounip'])
        except:
            pass

        resultados = []
        
        for item in datos_fisicos:
            if item.get('referencia') == 'Saldo Previo':
                item['precompra'] = float(costopro)
                item['entrada_valorada'] = 0.0
                item['salida_valorada'] = 0.0
                item['saldo_valorado'] = float(acum_entrada_valorada - acum_salida_valorada)
                item['costopromedio'] = float(costopro)
                resultados.append(item)
                continue

            # Obtener cantidades
            try:
                cnt = to_decimal(item.get('cantidad') or 0)
            except (TypeError, ValueError):
                cnt = Decimal('0')

            tipomov = (item.get('tipomovimiento') or '').upper()
            entrada_d = abs(cnt) if tipomov == 'ENTRADA' else Decimal('0')
            salida_d = abs(cnt) if tipomov == 'SALIDA' else Decimal('0')

            # Obtener precompra del item
            raw_pre = item.get('precompra', None)
            precompra = None if raw_pre in (None, '') else to_decimal(raw_pre)

            # Variables para cálculo
            tipo = item.get('tipo')
            idguiar = item.get('idguiar')
            
            # Obtener costo de guía si existe
            costo_guia = Decimal('0')
            if idguiar:
                try:
                    idg_tmp = int(idguiar)
                    idart_tmp = int(idarticulo)
                    costo_guia = guias_cost_map.get((idg_tmp, idart_tmp), Decimal('0'))
                except (TypeError, ValueError):
                    costo_guia = Decimal('0')

            # Obtener costo de OP si existe
            costo_op = Decimal('0')
            if tipo == 'op_ingreso' and item.get('numero'):
                try:
                    idorden_int = int(item.get('numero'))
                    idart_int = int(idarticulo)
                    costo_op = op_cost_map.get((idorden_int, idart_int), Decimal('0'))
                except (TypeError, ValueError):
                    costo_op = Decimal('0')

            # Costo para valorización (costo promedio antes de la operación)
            costo_para_valorizacion = costopro
            
            # Variables para resultado
            entval = Decimal('0')
            salval = Decimal('0')
            unit_price = costopro  # Precio unitario por defecto

            # Cantidad antes de la operación
            prev_qty = acum_entrada_fisica - acum_salida_fisica

            # ---- LÓGICA DE VALORIZACIÓN SEGÚN TIPO (IGUAL QUE EN _valorizar_movimientos) ----
            if tipo == 'compra':
                # Usar precompra si existe, sino costopro
                if precompra is not None:
                    unit_price = precompra
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)  # Mostrar precio de compra

            elif tipo == 'ajuste_entrada':
                # Usar precompra si existe, sino costopro
                if precompra is not None:
                    unit_price = precompra
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)  # Mostrar precio de ajuste

            elif tipo == 'guia_destino':
                # Usar costo de guía si existe, sino costopro
                if costo_guia > 0:
                    unit_price = costo_guia
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo == 'op_ingreso':
                # Usar costo de OP si existe, sino costopro
                if costo_op > 0:
                    unit_price = costo_op
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo == 'nota_credito':
                # Notas de crédito usan costopro para valorización
                unit_price = costopro
                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
                
                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo in ('venta', 'ajuste_salida', 'guia_origen', 'op_salida'):
                # SALIDAS: usar costo promedio vigente ANTES de la operación
                salval = (costo_para_valorizacion * salida_d).quantize(Decimal('0.00001'))
                acum_salida_fisica += salida_d
                acum_salida_valorada += salval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if abs(sal_val) < Decimal('0.01'):
                    sal_val = Decimal('0')

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    new_costopro = costopro  # Mantener costo previo si no hay stock

                costopro = new_costopro
                
                # CORRECCIÓN IMPORTANTE: Mostrar el costo utilizado para la salida, no 0
                item['precompra'] = float(costo_para_valorizacion)
                item['salida_valorada'] = float(salval)

            else:
                # Fallback para tipos no reconocidos
                if entrada_d > 0:
                    unit_price = costopro
                    entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                    acum_entrada_fisica += entrada_d
                    acum_entrada_valorada += entval
                else:
                    salval = (costo_para_valorizacion * salida_d).quantize(Decimal('0.00001'))
                    acum_salida_fisica += salida_d
                    acum_salida_valorada += salval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
                
                if abs(sal_val) < Decimal('0.01'):
                    sal_val = Decimal('0')
                
                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            # Protección contra costo promedio negativo
            if costopro < 0:
                costopro = Decimal('0')
                item['costopromedio'] = float(costopro)

            # Asegurar campos mostrados
            if 'entrada_valorada' not in item:
                item['entrada_valorada'] = float(entval)
            if 'salida_valorada' not in item:
                item['salida_valorada'] = float(salval)
            
            # Calcular saldo valorado actual
            saldo_valorado_actual = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
            item['saldo_valorado'] = float(saldo_valorado_actual)
            item['costopromedio'] = float(costopro)

            resultados.append(item)

        return resultados

    @staticmethod
    def _valorizar_movimientos_global(datos_fisicos, idarticulo, tipo_prioridad_map, fecha_inicio=None):
        """
        Valoriza los movimientos físicos GLOBALES aplicando costos promedio
        CORREGIDO: Usa misma lógica que _valorizar_movimientos pero para contexto global
        """
        from .models import DetOrdenProdIngreso, Detguia
        from decimal import Decimal
        
        to_decimal = KardexGeneralService.to_decimal_safe
        
        # Buscar saldo previo
        saldo_prev_item = next((i for i in datos_fisicos if i.get('referencia') == 'Saldo Previo'), None)
        
        # Inicializar acumuladores (igual que en _valorizar_movimientos)
        acum_entrada_fisica = Decimal('0')
        acum_salida_fisica = Decimal('0')
        acum_entrada_valorada = Decimal('0')
        acum_salida_valorada = Decimal('0')
        costopro = Decimal('0')

        if saldo_prev_item:
            saldo_fisico_inicial = to_decimal(saldo_prev_item.get('saldoinventario'))

            fecha_primer_mov = None
            for item in datos_fisicos:
                if item.get('referencia') != 'Saldo Previo':
                    fecha_primer_mov = item.get('fecha')
                    break
            
            fecha_para_costo = fecha_primer_mov or fecha_inicio

            # ✅ CORRECCIÓN GLOBAL
            if fecha_para_costo and saldo_fisico_inicial > 0:
                try:
                    costopro = KardexGeneralService._obtener_costo_promedio_a_fecha_global(
                        idarticulo=idarticulo,
                        fecha_limite=fecha_para_costo,
                        prioridad_map=tipo_prioridad_map
                    )
                except Exception as e:
                    print(f"ERROR costo previo global artículo {idarticulo}: {e}")
                    costopro = Decimal('0')

            saldo_valorado_inicial = costopro * saldo_fisico_inicial
            acum_entrada_fisica   = saldo_fisico_inicial
            acum_entrada_valorada = saldo_valorado_inicial

            saldo_prev_item['costopromedio'] = float(costopro)
            saldo_prev_item['saldo_valorado'] = float(saldo_valorado_inicial)

        # Prefetch de costos de guías (GLOBAL)
        guias_cost_map = {}
        try:
            guias_costos = Detguia.objects.filter(costo__gt=0).values('idguiar', 'idproduct', 'costo')
            for g in guias_costos:
                key = (int(g['idguiar']), int(g['idproduct']))
                guias_cost_map[key] = to_decimal(g['costo'])
        except:
            pass

        # Prefetch de costos de OP (GLOBAL)
        op_cost_map = {}
        try:
            op_costos = DetOrdenProdIngreso.objects.filter(costounip__gt=0).values('idorden_id', 'idproduct_id', 'costounip')
            for op in op_costos:
                key = (op['idorden_id'], op['idproduct_id'])
                op_cost_map[key] = to_decimal(op['costounip'])
        except:
            pass

        resultados = []
        
        for item in datos_fisicos:
            if item.get('referencia') == 'Saldo Previo':
                item['precompra'] = float(costopro)
                item['entrada_valorada'] = 0.0
                item['salida_valorada'] = 0.0
                item['saldo_valorado'] = float(acum_entrada_valorada - acum_salida_valorada)
                item['costopromedio'] = float(costopro)
                resultados.append(item)
                continue

            # Obtener cantidades
            try:
                cnt = to_decimal(item.get('cantidad') or 0)
            except (TypeError, ValueError):
                cnt = Decimal('0')

            tipomov = (item.get('tipomovimiento') or '').upper()
            entrada_d = abs(cnt) if tipomov == 'ENTRADA' else Decimal('0')
            salida_d = abs(cnt) if tipomov == 'SALIDA' else Decimal('0')

            # Obtener precompra del item
            raw_pre = item.get('precompra', None)
            precompra = None if raw_pre in (None, '') else to_decimal(raw_pre)

            # Variables para cálculo
            tipo = item.get('tipo')
            idguiar = item.get('idguiar')
            
            # Obtener costo de guía si existe
            costo_guia = Decimal('0')
            if idguiar:
                try:
                    idg_tmp = int(idguiar)
                    idart_tmp = int(idarticulo)
                    costo_guia = guias_cost_map.get((idg_tmp, idart_tmp), Decimal('0'))
                except (TypeError, ValueError):
                    costo_guia = Decimal('0')

            # Obtener costo de OP si existe
            costo_op = Decimal('0')
            if tipo == 'op_ingreso' and item.get('numero'):
                try:
                    idorden_int = int(item.get('numero'))
                    idart_int = int(idarticulo)
                    costo_op = op_cost_map.get((idorden_int, idart_int), Decimal('0'))
                except (TypeError, ValueError):
                    costo_op = Decimal('0')

            # Costo para valorización (costo promedio antes de la operación)
            costo_para_valorizacion = costopro
            
            # Variables para resultado
            entval = Decimal('0')
            salval = Decimal('0')
            unit_price = costopro  # Precio unitario por defecto

            # Cantidad antes de la operación
            prev_qty = acum_entrada_fisica - acum_salida_fisica

            # ---- LÓGICA DE VALORIZACIÓN SEGÚN TIPO (IGUAL QUE EN _valorizar_movimientos) ----
            if tipo == 'compra':
                # Usar precompra si existe, sino costopro
                if precompra is not None:
                    unit_price = precompra
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)  # Mostrar precio de compra

            elif tipo == 'ajuste_entrada':
                # Usar precompra si existe, sino costopro
                if precompra is not None:
                    unit_price = precompra
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)  # Mostrar precio de ajuste

            elif tipo == 'guia_destino':
                # Usar costo de guía si existe, sino costopro
                if costo_guia > 0:
                    unit_price = costo_guia
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo == 'op_ingreso':
                # Usar costo de OP si existe, sino costopro
                if costo_op > 0:
                    unit_price = costo_op
                else:
                    unit_price = costopro

                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo == 'nota_credito':
                # Notas de crédito usan costopro para valorización
                unit_price = costopro
                entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                acum_entrada_fisica += entrada_d
                acum_entrada_valorada += entval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
                
                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    if prev_qty <= Decimal('0'):
                        new_costopro = unit_price
                    else:
                        new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            elif tipo in ('venta', 'ajuste_salida', 'guia_origen', 'op_salida'):
                # SALIDAS: usar costo promedio vigente ANTES de la operación
                salval = (costo_para_valorizacion * salida_d).quantize(Decimal('0.00001'))
                acum_salida_fisica += salida_d
                acum_salida_valorada += salval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))

                if abs(sal_val) < Decimal('0.01'):
                    sal_val = Decimal('0')

                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    new_costopro = costopro  # Mantener costo previo si no hay stock

                costopro = new_costopro
                
                # CORRECCIÓN IMPORTANTE: Mostrar el costo utilizado para la salida, no 0
                item['precompra'] = float(costo_para_valorizacion)
                item['salida_valorada'] = float(salval)

            else:
                # Fallback para tipos no reconocidos
                if entrada_d > 0:
                    unit_price = costopro
                    entval = (unit_price * entrada_d).quantize(Decimal('0.00001'))
                    acum_entrada_fisica += entrada_d
                    acum_entrada_valorada += entval
                else:
                    salval = (costo_para_valorizacion * salida_d).quantize(Decimal('0.00001'))
                    acum_salida_fisica += salida_d
                    acum_salida_valorada += salval

                # Recalcular costo promedio
                sal_fis = acum_entrada_fisica - acum_salida_fisica
                sal_val = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
                
                if abs(sal_val) < Decimal('0.01'):
                    sal_val = Decimal('0')
                
                if sal_fis > 0:
                    new_costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
                else:
                    new_costopro = costopro

                costopro = new_costopro
                item['precompra'] = float(unit_price)

            # Protección contra costo promedio negativo
            if costopro < 0:
                costopro = Decimal('0')
                item['costopromedio'] = float(costopro)

            # Asegurar campos mostrados
            if 'entrada_valorada' not in item:
                item['entrada_valorada'] = float(entval)
            if 'salida_valorada' not in item:
                item['salida_valorada'] = float(salval)
            
            # Calcular saldo valorado actual
            saldo_valorado_actual = (acum_entrada_valorada - acum_salida_valorada).quantize(Decimal('0.00001'))
            item['saldo_valorado'] = float(saldo_valorado_actual)
            item['costopromedio'] = float(costopro)

            resultados.append(item)

        return resultados

    
    @staticmethod
    def generar_kardex_general_global(fecha_inicio, fecha_fin, batch_size=100):
        """
        Genera kardex general GLOBAL para todos los artículos (todas las sucursales)
        ACTUALIZADO: Incluye valorización de OP
        """
        from .models import Articulos
        
        print(f"INICIANDO KARDEX GENERAL GLOBAL CON OP")
        print(f"- Fecha: {fecha_inicio} a {fecha_fin}")
        print(f"- Sucursales: TODAS")

        resultados = []
        articulos = Articulos.objects.all().select_related('unidad')
        total_articulos = articulos.count()
        
        print(f"PROCESANDO {total_articulos} ARTÍCULOS...")

        for i in range(0, total_articulos, batch_size):
            batch = articulos[i:i + batch_size]
            print(f"Lote {(i//batch_size)+1}/{(total_articulos+batch_size-1)//batch_size}")
            
            for articulo in batch:
                try:
                    movimientos = KardexGeneralService.calcular_kardex_valorado_por_articulo_global(
                        idarticulo=articulo.idproduct,
                        fecha_inicio=fecha_inicio,
                        fecha_fin=fecha_fin
                    )
                    
                    if movimientos:
                        data_final={
                            'idproduct': articulo.idproduct,
                            'codigo': getattr(articulo, 'codigo', None),
                            'nombre': getattr(articulo, 'nombre', ''),
                            'tipo_exis': getattr(articulo, 'tipo_exis', '01'),
                            'unidad': articulo.unidad.unidad if articulo.unidad else '',
                            'movimientos': movimientos,
                            'total_movimientos': len(movimientos)
                        }
                        print(f"--> ARTICULO: {data_final['idproduct']} | TIPO_EXIS: {data_final['tipo_exis']}")
                        resultados.append(data_final)
                                                
                except Exception as e:
                    print(f"Error en artículo {articulo.idproduct}: {str(e)}")

        print(f"COMPLETADO: {len(resultados)}/{total_articulos} artículos con movimientos")
        
        return resultados

class KardexExcelService:
    
    @staticmethod
    def generar_excel_kardex_valorado(idarticulo, fecha_inicio, fecha_fin, idsucursal):
        """
        Genera datos para Excel de kardex valorado individual
        Reemplaza la lógica de KardexValoradoExcelAPIView
        """
        from .models import Articulos, Tip_exitbl05
        
        # Validar artículo
        try:
            articulo = Articulos.objects.select_related('unidad').get(idproduct=idarticulo)
        except Articulos.DoesNotExist:
            return None

        # Obtener descripción de tipo_exis
        tipo_exis_desc = ''
        if articulo.tipo_exis:
            try:
                tipo_exis_obj = Tip_exitbl05.objects.get(cod=articulo.tipo_exis)
                tipo_exis_desc = tipo_exis_obj.descripcion or ''
            except Tip_exitbl05.DoesNotExist:
                tipo_exis_desc = ''

        # Obtener movimientos usando el servicio existente
        movimientos = KardexGeneralService.calcular_kardex_valorado_por_articulo(
            idarticulo=idarticulo,
            fecha_inicio=fecha_inicio,
            fecha_fin=fecha_fin,
            idsucursal=idsucursal
        )

        if not movimientos:
            return None

        # Preparar datos para Excel
        excel_data = {
            'articulo': {
                'id': articulo.idproduct,
                'codigo': getattr(articulo, 'codigo', ''),
                'nombre': getattr(articulo, 'nombre', ''),
                'unidad': articulo.unidad.unidad if articulo.unidad else '',
                'tipo_exis': articulo.tipo_exis if articulo.tipo_exis else '',
                'tipo_exis_desc': tipo_exis_desc
            },
            'movimientos': KardexExcelService._formatear_movimientos_excel(movimientos),
            'config': {
                'fecha_inicio': fecha_inicio,
                'fecha_fin': fecha_fin,
                'sucursal': idsucursal
            }
        }

        return excel_data

    @staticmethod
    def generar_excel_kardex_general(fecha_inicio, fecha_fin, idsucursal, batch_size=50):
        """
        Genera datos para Excel de kardex general
        Reemplaza la lógica de KardexGeneralExcelAPIView
        """
        from .models import Articulos, Tip_exitbl05
        
        resultados = []
        articulos = Articulos.objects.all().select_related('unidad')
        
        # Precarga todas las descripciones de tipo_exis para optimizar
        tipos_exis = {t.cod: t.descripcion for t in Tip_exitbl05.objects.all()}

        for articulo in articulos:
            try:
                movimientos = KardexGeneralService.calcular_kardex_valorado_por_articulo(
                    idarticulo=articulo.idproduct,
                    fecha_inicio=fecha_inicio,
                    fecha_fin=fecha_fin,
                    idsucursal=idsucursal
                )

                if movimientos:
                    # Obtener descripción de tipo_exis desde el diccionario precargado
                    tipo_exis_desc = tipos_exis.get(articulo.tipo_exis, '') if articulo.tipo_exis else ''
                    
                    resultados.append({
                        'codigo': getattr(articulo, 'codigo', ''),
                        'nombre': getattr(articulo, 'nombre', ''),
                        'unidad': articulo.unidad.unidad if articulo.unidad else '',
                        'tipo_exis': articulo.tipo_exis if articulo.tipo_exis else '',
                        'tipo_exis_desc': tipo_exis_desc,
                        'movimientos': KardexExcelService._formatear_movimientos_excel(movimientos)
                    })
                    
            except Exception as e:
                print(f"Error procesando artículo {articulo.idproduct} para Excel: {str(e)}")
                continue

        return {
            'articulos': resultados,
            'config': {
                'fecha_inicio': fecha_inicio,
                'fecha_fin': fecha_fin,
                'sucursal': idsucursal,
                'total_articulos': len(resultados)
            }
        }

    @staticmethod
    def _formatear_movimientos_excel(movimientos):
        """
        Formatea los movimientos para el formato de Excel
        """
        if movimientos:
            print("🔑 KEYS REALES:", list(movimientos[0].keys()))
            print("📦 PRIMER MOV:", movimientos[0])
        
        
        from datetime import datetime
        
        movimientos_formateados = []
        
        for mov in movimientos:
            # Procesar fecha
            raw_fecha = mov.get('fecha')
            if isinstance(raw_fecha, str):
                try:
                    fecha_dt = datetime.strptime(raw_fecha, '%Y-%m-%d').date()
                    fecha_str = fecha_dt.strftime('%d-%m-%Y')
                except:
                    fecha_str = raw_fecha
            elif hasattr(raw_fecha, 'strftime'):
                fecha_str = raw_fecha.strftime('%d-%m-%Y')
            else:
                fecha_str = str(raw_fecha)

            # Cantidad física
            cantidad = float(mov.get('cantidad', 0) or 0)
            
            # Costo unitario
            costo_unit = float(mov.get('precompra', mov.get('costopromedio', 0)) or 0)
            
            # Valores valorados
            entrada_val = float(mov.get('entrada_valorada', 0) or 0)
            salida_val = float(mov.get('salida_valorada', 0) or 0)
            
            # Saldos
            saldo_fis = float(mov.get('saldoinventario', 0) or 0)
            costo_prom = float(mov.get('costopromedio', 0) or 0)
            saldo_val = float(mov.get('saldo_valorado', 0) or 0)

            # Determinar columnas de ENTRADA y SALIDA
            tipomovimiento = mov.get('tipomovimiento', '')
            ent_cant = cantidad if tipomovimiento == 'ENTRADA' else 0
            sal_cant = cantidad if tipomovimiento == 'SALIDA' else 0
            ent_costo = costo_unit if ent_cant else 0
            sal_costo = costo_unit if sal_cant else 0
            ent_saldo = entrada_val if ent_cant else 0
            sal_saldo = salida_val if sal_cant else 0

            movimientos_formateados.append({
                'fecha': fecha_str,
                'documento': mov.get('documento', ''),
                'serie': mov.get('serie', ''),
                'numero': mov.get('numero', ''),
                'ent_cantidad': ent_cant,
                'ent_costo': ent_costo,
                'ent_saldo': ent_saldo,
                'sal_cantidad': sal_cant,
                'sal_costo': sal_costo,
                'sal_saldo': sal_saldo,
                'saldo': saldo_fis,
                'costo': costo_prom,
                'saldo_valorado': saldo_val,
                'referencia': mov.get('referencia', ''),
                'tipo_movimiento': tipomovimiento,
            })

        return movimientos_formateados

    @staticmethod
    def crear_archivo_excel_kardex_valorado(excel_data, output_path):
        """
        Crea el archivo Excel para kardex valorado individual
        """
        import xlsxwriter
    
        wb = xlsxwriter.Workbook(output_path)
        header_fmt = wb.add_format({'bold': True, 'bg_color': '#D3D3D3'})
    
        # Limpiar nombre de hoja
        nombre_hoja = excel_data['articulo']['nombre'][:31] if excel_data['articulo']['nombre'] else f"Art-{excel_data['articulo']['id']}"
        caracteres_invalidos = ['[', ']', ':', '*', '?', '/', '\\']
        for char in caracteres_invalidos:
            nombre_hoja = nombre_hoja.replace(char, '_')
    
        ws = wb.add_worksheet(nombre_hoja)
        
        # Encabezados (añadida columna Tipo Existencia)
        headers = [
            'Código', 'Nombre', 'Tipo Exist.',
            'Fecha', 'Documento', 'Serie', 'Número',
            'EntCantidad', 'EntCosto', 'EntSaldo',
            'SalCantidad', 'SalCosto', 'SalidaSaldo',
            'Saldo', 'Costo', 'SaldoValorado'
        ]
        ws.write_row(0, 0, headers, header_fmt)
        
        # Obtener datos del artículo
        codigo_articulo = excel_data['articulo'].get('codigo', '')
        nombre_articulo = excel_data['articulo'].get('nombre', '')
        tipo_exis_desc = excel_data['articulo'].get('tipo_exis_desc', '')
        
        # Datos
        for idx, mov in enumerate(excel_data['movimientos'], start=1):
            row = [
                codigo_articulo,
                nombre_articulo,
                tipo_exis_desc,  # Nueva columna
                mov['fecha'],
                mov['documento'],
                mov['serie'],
                mov['numero'],
                mov['ent_cantidad'],
                mov['ent_costo'],
                mov['ent_saldo'],
                mov['sal_cantidad'],
                mov['sal_costo'],
                mov['sal_saldo'],
                mov['saldo'],
                mov['costo'],
                mov['saldo_valorado']
            ]
            ws.write_row(idx, 0, row)
        
        wb.close()
        return output_path
    
    
    @staticmethod
    def crear_archivo_excel_kardex_general(excel_data, output_path):
        """
        Crea el archivo Excel para kardex general
        """
        import xlsxwriter
        
        wb = xlsxwriter.Workbook(output_path)
        bold_fmt = wb.add_format({'bold': True})
        header_fmt = wb.add_format({'bold': True, 'bg_color': '#D3D3D3'})

        ws = wb.add_worksheet('KardexGeneral')
        row = 0

        for prod in excel_data['articulos']:
            # Fila de datos del producto
            ws.write(row, 0, 'Código:', bold_fmt)
            ws.write(row, 1, prod['codigo'])
            ws.write(row, 2, 'Nombre:', bold_fmt)
            ws.write(row, 3, prod['nombre'])
            ws.write(row, 4, 'Unidad:', bold_fmt)
            ws.write(row, 5, prod['unidad'])
            ws.write(row, 6, 'Tipo Exist.:', bold_fmt)
            ws.write(row, 7, prod.get('tipo_exis_desc', ''))
            row += 2

            # Encabezados de movimientos
            headers = [
                'Fecha', 'Documento', 'Serie', 'Número',
                'EntCantidad', 'EntCosto', 'EntSaldo',
                'SalCantidad', 'SalCosto', 'SalidaSaldo',
                'Saldo', 'Costo', 'SaldoValorado'
            ]
            ws.write_row(row, 0, headers, header_fmt)
            row += 1

            # Filas de movimientos
            for mov in prod['movimientos']:
                row_data = [
                    mov['fecha'],
                    mov['documento'],
                    mov['serie'],
                    mov['numero'],
                    mov['ent_cantidad'],
                    mov['ent_costo'],
                    mov['ent_saldo'],
                    mov['sal_cantidad'],
                    mov['sal_costo'],
                    mov['sal_saldo'],
                    mov['saldo'],
                    mov['costo'],
                    mov['saldo_valorado']
                ]
                ws.write_row(row, 0, row_data)
                row += 1

            row += 2  # Espacio entre artículos

        wb.close()
        return output_path
    
    @staticmethod
    def crear_archivo_excel_kardex_general_con_tabla(excel_data, output_path):
        """
        Versión alternativa usando Tablas de Excel (recomendado)
        Las tablas de Excel tienen filtros nativos y mejor UX
        """
        import xlsxwriter
        
        wb = xlsxwriter.Workbook(output_path)
        ws = wb.add_worksheet('KardexGeneral')
        
        # Formatos
        header_fmt = wb.add_format({
            'bold': True,
            'bg_color': '#4472C4',
            'font_color': 'white',
            'border': 1
        })
        
        number_fmt = wb.add_format({'num_format': '#,##0.000'})
        
        # Cabeceras (añadida Tipo Exist.)
        headers = [
            'Código', 'Nombre', 'Tipo Exist.', 'Unidad', 'Fecha', 'Documento', 'Serie', 'Número',
            'Referencia', 'Entrada Previa', 'Costo Previo',
            'EntCantidad', 'EntCosto', 'EntSaldo',
            'SalCantidad', 'SalCosto', 'SalidaSaldo',
            'Saldo', 'Costo', 'SaldoValorado'
        ]
        
        # Preparar datos para la tabla
        data = []
        for prod in excel_data['articulos']:
            for mov in prod['movimientos']:
                data.append([
                    prod['codigo'],
                    prod['nombre'],
                    prod.get('tipo_exis_desc', ''),  # Nueva columna
                    prod['unidad'],
                    mov['fecha'],
                    mov['documento'],
                    mov['serie'],
                    mov['numero'],
                    mov.get('referencia', ''),
                    mov['saldo'],
                    mov['costo'],
                    mov['ent_cantidad'],
                    mov['ent_costo'],
                    mov['ent_saldo'],
                    mov['sal_cantidad'],
                    mov['sal_costo'],
                    mov['sal_saldo'],
                    mov['saldo'],
                    mov['costo'],
                    mov['saldo_valorado']
                ])
        
        # Crear tabla de Excel
        if data:
            # Configuración de columnas para la tabla
            columns = []
            for i, header in enumerate(headers):
                col_config = {'header': header}
                
                # Aplicar formato numérico a columnas de números
                if i >= 9:  # Columnas numéricas (ajustado por nueva columna)
                    col_config['format'] = number_fmt
                
                columns.append(col_config)
            
            # Crear la tabla
            ws.add_table(0, 0, len(data), len(headers) - 1, {
                'name': 'TablaKardex',
                'style': 'Table Style Medium 2',
                'columns': columns,
                'data': data
            })
            
            # Ajustar anchos
            ws.set_column(0, 0, 12)   # Código
            ws.set_column(1, 1, 35)   # Nombre
            ws.set_column(2, 2, 15)   # Tipo Exist.
            ws.set_column(3, 3, 10)   # Unidad
            ws.set_column(4, 4, 12)   # Fecha
            ws.set_column(5, 5, 12)   # Documento
            ws.set_column(6, 8, 12)   # Serie, Número, Referencia
            ws.set_column(9, 19, 13)  # Columnas numéricas
            
            # Congelar primera fila
            ws.freeze_panes(1, 0)
        
        wb.close()
        return output_path


    @staticmethod
    def _formatear_movimientos_excel_con_tabla(movimientos):
        """
        Formatea los movimientos para el formato de Excel
        ACTUALIZADO: Incluye campo 'referencia'
        """
        from datetime import datetime
        
        movimientos_formateados = []
        
        for mov in movimientos:
            # Procesar fecha
            raw_fecha = mov.get('fecha')
            if isinstance(raw_fecha, str):
                try:
                    fecha_dt = datetime.strptime(raw_fecha, '%Y-%m-%d').date()
                    fecha_str = fecha_dt.strftime('%d/%m/%Y')
                except:
                    fecha_str = raw_fecha
            elif hasattr(raw_fecha, 'strftime'):
                fecha_str = raw_fecha.strftime('%d/%m/%Y')
            else:
                fecha_str = str(raw_fecha)

            # Cantidad física
            cantidad = float(mov.get('cantidad', 0) or 0)
            
            # Costo unitario
            costo_unit = float(mov.get('precompra', mov.get('costopromedio', 0)) or 0)
            
            # Valores valorados
            entrada_val = float(mov.get('entrada_valorada', 0) or 0)
            salida_val = float(mov.get('salida_valorada', 0) or 0)
            
            # Saldos
            saldo_fis = float(mov.get('saldoinventario', 0) or 0)
            costo_prom = float(mov.get('costopromedio', 0) or 0)
            saldo_val = float(mov.get('saldo_valorado', 0) or 0)

            # Determinar columnas de ENTRADA y SALIDA
            tipomovimiento = mov.get('tipomovimiento', '')
            ent_cant = cantidad if tipomovimiento == 'ENTRADA' else 0
            sal_cant = cantidad if tipomovimiento == 'SALIDA' else 0
            ent_costo = costo_unit if ent_cant else 0
            sal_costo = costo_unit if sal_cant else 0
            ent_saldo = entrada_val if ent_cant else 0
            sal_saldo = salida_val if sal_cant else 0

            movimientos_formateados.append({
                'fecha': fecha_str,
                'documento': mov.get('documento', ''),
                'serie': mov.get('serie', ''),
                'numero': str(mov.get('numero', '')),
                'referencia': mov.get('referencia', ''),
                'ent_cantidad': ent_cant,
                'ent_costo': ent_costo,
                'ent_saldo': ent_saldo,
                'sal_cantidad': sal_cant,
                'sal_costo': sal_costo,
                'sal_saldo': sal_saldo,
                'saldo': saldo_fis,
                'costo': costo_prom,
                'saldo_valorado': saldo_val,
                'tipo_movimiento': tipomovimiento
            })

        return movimientos_formateados
    
    @staticmethod
    def generar_excel_kardex_general_global(fecha_inicio, fecha_fin, batch_size=50):
        """
        Genera datos para Excel de kardex general GLOBAL (todas las sucursales).
        No modifica _formatear_movimientos_excel_con_tabla; inyecta 'sucursal' desde
        los movimientos raw (se asume que allí ya existe 'sucursal' con el nombre).
        """
        from .models import Articulos, Tip_exitbl05

        resultados = []
        articulos = Articulos.objects.all().select_related('unidad')

        # Precarga todas las descripciones de tipo_exis para optimizar
        tipos_exis = {t.cod: t.descripcion for t in Tip_exitbl05.objects.all()}

        sucursales_set = set()

        for articulo in articulos:
            try:
                movimientos_raw = KardexGeneralService.calcular_kardex_valorado_por_articulo_global(
                    idarticulo=articulo.idproduct,
                    fecha_inicio=fecha_inicio,
                    fecha_fin=fecha_fin
                )

                if movimientos_raw:
                    # Formatear usando la función existente (sin tocarla)
                    movimientos_formateados = KardexExcelService._formatear_movimientos_excel_con_tabla(movimientos_raw)

                    # Inyectar 'sucursal' en cada movimiento formateado a partir del movimiento raw.
                    # No hacemos validaciones; se asume que movimientos_raw[i]['sucursal'] existe.
                    for idx, fm in enumerate(movimientos_formateados):
                        suc = movimientos_raw[idx]['sucursal']    # acceso directo intencionado
                        fm['sucursal'] = suc
                        sucursales_set.add(suc)

                    tipo_exis_desc = tipos_exis.get(articulo.tipo_exis, '') if articulo.tipo_exis else ''

                    resultados.append({
                        'codigo': getattr(articulo, 'codigo', ''),
                        'nombre': getattr(articulo, 'nombre', ''),
                        'unidad': articulo.unidad.unidad if articulo.unidad else '',
                        'tipo_exis': articulo.tipo_exis if articulo.tipo_exis else '',
                        'tipo_exis_desc': tipo_exis_desc,
                        'movimientos': movimientos_formateados
                    })

            except Exception as e:
                # Mantengo el comportamiento previo de seguir con el siguiente artículo
                print(f"Error procesando artículo {articulo.idproduct} para Excel Global: {str(e)}")
                continue

        sucursales_list = sorted(sucursales_set)
        sucursales_str = ', '.join(sucursales_list) if sucursales_list else 'TODAS'

        return {
            'articulos': resultados,
            'config': {
                'fecha_inicio': fecha_inicio,
                'fecha_fin': fecha_fin,
                'sucursales': sucursales_str,
                'total_articulos': len(resultados)
            }
        }


    @staticmethod
    def crear_archivo_excel_kardex_general_global_con_tabla(excel_data, output_path):
        """
        Crea archivo Excel de kardex general GLOBAL usando Tablas de Excel.
        Se añade la columna 'Sucursal' (nombre) y se aplican formatos numéricos desde la
        columna de cantidades en adelante.
        """
        import xlsxwriter

        wb = xlsxwriter.Workbook(output_path)
        ws = wb.add_worksheet('KardexGeneralGlobal')

        # Formatos
        header_fmt = wb.add_format({
            'bold': True,
            'bg_color': '#4472C4',
            'font_color': 'white',
            'border': 1
        })

        number_fmt = wb.add_format({'num_format': '#,##0.000'})

        # Cabeceras (añadida la columna 'Sucursal' después de 'Unidad')
        headers = [
            'Código', 'Nombre', 'Tipo Exist.', 'Unidad', 'Sucursal', 'Fecha', 'Documento', 'Serie', 'Número',
            'Referencia', 'EntCantidad', 'EntCosto', 'EntSaldo',
            'SalCantidad', 'SalCosto', 'SalidaSaldo',
            'Saldo', 'Costo', 'SaldoValorado'
        ]

        # Preparar datos para la tabla
        data = []
        for prod in excel_data['articulos']:
            for mov in prod['movimientos']:
                # Usamos acceso directo a mov['sucursal'] para respetar la semántica "debe existir"
                data.append([
                    prod['codigo'],
                    prod['nombre'],
                    prod.get('tipo_exis_desc', ''),
                    prod['unidad'],
                    mov['sucursal'],
                    mov.get('fecha', ''),
                    mov.get('documento', ''),
                    mov.get('serie', ''),
                    mov.get('numero', ''),
                    mov.get('referencia', ''),
                    mov.get('ent_cantidad', 0.0),
                    mov.get('ent_costo', 0.0),
                    mov.get('ent_saldo', 0.0),
                    mov.get('sal_cantidad', 0.0),
                    mov.get('sal_costo', 0.0),
                    mov.get('sal_saldo', 0.0),
                    mov.get('saldo', 0.0),
                    mov.get('costo', 0.0),
                    mov.get('saldo_valorado', 0.0)
                ])

        # Crear tabla de Excel
        if data:
            # Configuración de columnas para la tabla
            columns = []
            for i, header in enumerate(headers):
                col_config = {'header': header}
                # Aplicar formato numérico a columnas de números:
                # las columnas numéricas comienzan en el índice 10 (0-based) porque añadimos 'Sucursal'
                if i >= 10:
                    col_config['format'] = number_fmt
                columns.append(col_config)

            ws.add_table(0, 0, len(data), len(headers) - 1, {
                'name': 'TablaKardexGlobal',
                'style': 'Table Style Medium 2',
                'columns': columns,
                'data': data
            })

            # Ajustar anchos
            ws.set_column(0, 0, 12)   # Código
            ws.set_column(1, 1, 35)   # Nombre
            ws.set_column(2, 2, 15)   # Tipo Exist.
            ws.set_column(3, 4, 14)   # Unidad + Sucursal
            ws.set_column(5, 5, 12)   # Fecha
            ws.set_column(6, 8, 12)   # Documento, Serie, Número
            ws.set_column(9, 9, 20)   # Referencia
            ws.set_column(10, 18, 13)  # Columnas numéricas

            # Congelar primera fila
            ws.freeze_panes(1, 0)
            wb.close()
        return output_path


def calcular_costo_para_guia(idarticulo, fecha, idsucursal):
    """
    Calcula el costo promedio de un artículo en una sucursal hasta una fecha dada,
    incluyendo las órdenes de producción del mismo día (fecha__lte).
    Se usa al crear un detguia para guardar el costo en el momento del traslado.
    """
    from datetime import datetime

    to_decimal = KardexGeneralService.to_decimal_safe

    if isinstance(fecha, str):
        fecha = datetime.strptime(fecha, '%Y-%m-%d').date()

    prioridad_map = {
        'ajuste_entrada': 0,
        'compra': 1,
        'guia_destino': 2,
        'op_ingreso': 2,
        'nota_credito': 3,
        'ajuste_salida': 4,
        'guia_origen': 5,
        'op_salida': 5,
        'venta': 6,
    }

    movimientos = []

    # COMPRAS (hasta la fecha, inclusive)
    for c in Detcompra.objects.filter(
        idproduct=idarticulo,
        idcompra__fecemi__lte=fecha,
        idcompra__idsucursal=idsucursal,
    ).exclude(idcompra__estado='ANU').select_related('idcompra'):
        cantidad = to_decimal(c.cantidad)
        precio = to_decimal(c.precio)
        flete = to_decimal(c.flete)
        otros = to_decimal(c.otros)
        tcambio = to_decimal(getattr(c.idcompra, 'tcambio', 1))
        flete_u = (flete / cantidad) if cantidad > 0 else Decimal('0')
        otros_u = (otros / cantidad) if cantidad > 0 else Decimal('0')
        precompra = (precio + flete_u + otros_u) * tcambio
        movimientos.append({
            'fecha': c.idcompra.fecemi,
            'hora': getattr(c.idcompra, 'hora', time(0, 0)),
            'tipo': 'compra',
            'cantidad': cantidad,
            'precio_unitario': precompra,
            'valor_total': precompra * cantidad,
            'prioridad': prioridad_map['compra'],
        })

    # AJUSTES (antes de la fecha)
    for aj in Detajuste.objects.filter(
        idproduct=idarticulo,
        idajuste__fecha__lt=fecha,
        idajuste__idsucursal=idsucursal,
    ).exclude(idajuste__estado='ANU').select_related('idajuste'):
        cantidad = to_decimal(aj.cantidad)
        valunit = to_decimal(aj.valunit)
        tipo_ope = getattr(aj.idajuste, 'tipo_ope', None)
        if tipo_ope in ['02','05', '16'] or (tipo_ope == '17' and cantidad >= 0):
            movimientos.append({
                'fecha': aj.idajuste.fecha,
                'hora': getattr(aj.idajuste, 'hora', time(0, 0)),
                'tipo': 'ajuste_entrada',
                'cantidad': abs(cantidad),
                'precio_unitario': valunit if valunit > 0 else Decimal('0'),
                'valor_total': valunit * abs(cantidad) if valunit > 0 else None,
                'prioridad': prioridad_map['ajuste_entrada'],
            })
        else:
            movimientos.append({
                'fecha': aj.idajuste.fecha,
                'hora': getattr(aj.idajuste, 'hora', time(0, 0)),
                'tipo': 'ajuste_salida',
                'cantidad': abs(cantidad),
                'precio_unitario': Decimal('0'),
                'valor_total': None,
                'prioridad': prioridad_map['ajuste_salida'],
            })

    # VENTAS (antes de la fecha)
    for v in Detfactu.objects.filter(
        idproduct=idarticulo,
        idfactura__fecha__lt=fecha,
        idfactura__idsucursal=idsucursal,
    ).exclude(Q(idfactura__estado='ANU') | Q(idfactura__documen='NCR')).select_related('idfactura'):
        movimientos.append({
            'fecha': v.idfactura.fecha,
            'hora': getattr(v.idfactura, 'hora', time(0, 0)),
            'tipo': 'venta',
            'cantidad': abs(to_decimal(v.cantidad)),
            'precio_unitario': Decimal('0'),
            'valor_total': None,
            'prioridad': prioridad_map['venta'],
        })

    # NOTAS DE CRÉDITO (antes de la fecha)
    for nc in Detfactu.objects.filter(
        idproduct=idarticulo,
        idfactura__fecha__lt=fecha,
        idfactura__idsucursal=idsucursal,
        idfactura__documen='NCR',
    ).exclude(
        Q(idfactura__motivo__in=['03', '04', '05', '08', '09', '13']) | Q(idfactura__estado='ANU')
    ).select_related('idfactura'):
        movimientos.append({
            'fecha': nc.idfactura.fecha,
            'hora': getattr(nc.idfactura, 'hora', time(0, 0)),
            'tipo': 'nota_credito',
            'cantidad': abs(to_decimal(nc.cantidad)),
            'precio_unitario': Decimal('0'),
            'valor_total': None,
            'prioridad': prioridad_map['nota_credito'],
        })

    # GUÍAS ORIGEN (antes de la fecha)
    for g in Detguia.objects.filter(
        idproduct=idarticulo,
        idguiar__fecha__lt=fecha,
        idguiar__idsucursal=idsucursal,
    ).exclude(Q(idguiar__motivo__in=['01', '14']) | Q(idguiar__estado='ANU')).select_related('idguiar'):
        movimientos.append({
            'fecha': g.idguiar.fecha,
            'hora': getattr(g.idguiar, 'hora', time(0, 0)),
            'tipo': 'guia_origen',
            'cantidad': abs(to_decimal(g.cantidad)),
            'precio_unitario': Decimal('0'),
            'valor_total': None,
            'prioridad': prioridad_map['guia_origen'],
        })

    # GUÍAS DESTINO (antes de la fecha)
    for g in Detguia.objects.filter(
        idproduct=idarticulo,
        idguiar__fecha__lt=fecha,
        idguiar__idsucursal_destino=idsucursal,
    ).exclude(idguiar__estado='ANU').select_related('idguiar'):
        cantidad = to_decimal(g.cantidad)
        costo_g = to_decimal(g.costo) if g.costo else Decimal('0')
        movimientos.append({
            'fecha': g.idguiar.fecha,
            'hora': getattr(g.idguiar, 'hora', time(0, 0)),
            'tipo': 'guia_destino',
            'cantidad': cantidad,
            'precio_unitario': costo_g,
            'valor_total': costo_g * cantidad if costo_g > 0 else None,
            'prioridad': prioridad_map['guia_destino'],
        })

    # OP INGRESOS (incluyendo mismo día: fecha__lte)
    for opi in DetOrdenProdIngreso.objects.filter(
        idproduct=idarticulo,
        fecha__lte=fecha,
        idorden__idsucursal=idsucursal,
    ).select_related('idorden'):
        produccion = to_decimal(opi.produccion)
        costounip = to_decimal(opi.costounip)
        precio_u = costounip if costounip > 0 else Decimal('0')
        movimientos.append({
            'fecha': opi.fecha,
            'hora': getattr(opi.idorden, 'hora', time(0, 0)),
            'tipo': 'op_ingreso',
            'cantidad': produccion,
            'precio_unitario': precio_u,
            'valor_total': precio_u * produccion if precio_u > 0 else None,
            'prioridad': prioridad_map['op_ingreso'],
        })

    # OP SALIDAS (antes de la fecha)
    for ops in DetOrdenProdSalida.objects.filter(
        idproduct=idarticulo,
        idorden__fecha__lt=fecha,
        idorden__idsucursal=idsucursal,
    ).select_related('idorden'):
        movimientos.append({
            'fecha': ops.idorden.fecha,
            'hora': getattr(ops.idorden, 'hora', time(0, 0)),
            'tipo': 'op_salida',
            'cantidad': abs(to_decimal(ops.cantidad)),
            'precio_unitario': Decimal('0'),
            'valor_total': None,
            'prioridad': prioridad_map['op_salida'],
        })

    # Ordenar
    movimientos.sort(key=lambda x: (x['fecha'], x['hora'] or time(0, 0), x['prioridad']))

    # Calcular costo promedio progresivo
    acum_ent_fis = Decimal('0')
    acum_sal_fis = Decimal('0')
    acum_ent_val = Decimal('0')
    acum_sal_val = Decimal('0')
    costopro = Decimal('0')

    for mov in movimientos:
        tipo = mov['tipo']
        cantidad = abs(mov['cantidad'])
        precio_unitario = mov['precio_unitario']
        prev_qty = acum_ent_fis - acum_sal_fis

        if tipo == 'compra':
            entval = (precio_unitario * cantidad).quantize(Decimal('0.00001'))
            acum_ent_fis += cantidad
            acum_ent_val += entval

        elif tipo == 'ajuste_entrada':
            entval = (
                (precio_unitario * cantidad).quantize(Decimal('0.00001'))
                if precio_unitario > 0
                else (costopro * cantidad).quantize(Decimal('0.00001'))
            )
            acum_ent_fis += cantidad
            acum_ent_val += entval

        elif tipo in {'guia_destino', 'nota_credito', 'op_ingreso'}:
            entval = (
                (precio_unitario * cantidad).quantize(Decimal('0.00001'))
                if precio_unitario > 0
                else (costopro * cantidad).quantize(Decimal('0.00001'))
            )
            acum_ent_fis += cantidad
            acum_ent_val += entval

        elif tipo in {'venta', 'ajuste_salida', 'guia_origen', 'op_salida'}:
            salval = (costopro * cantidad).quantize(Decimal('0.00001'))
            acum_sal_fis += cantidad
            acum_sal_val += salval

        sal_fis = acum_ent_fis - acum_sal_fis
        sal_val = (acum_ent_val - acum_sal_val).quantize(Decimal('0.00001'))
        if abs(sal_val) < Decimal('0.01'):
            sal_val = Decimal('0')

        if sal_fis > 0:
            costopro = (sal_val / sal_fis).quantize(Decimal('0.00001'))
        elif prev_qty <= 0 and tipo in {'compra', 'ajuste_entrada'}:
            costopro = precio_unitario if precio_unitario > 0 else costopro

        if costopro < 0:
            costopro = Decimal('0')

    return costopro
