from django.db.models import Q, F from collections import defaultdict, Counter from decimal import Decimal from inventory.models import * from sales.models import * from stock.models import * from material.models import * from supplier.models import * from masters.models import * from common.utils import getItemNameById, format_amount, get_final_price_for_imei from store.models import * from wholesale.models import * # --------------------------------------------------------- # 🔹 Helper: get EAN and HSN if missing # --------------------------------------------------------- def get_ean_hsn(row): """ Returns a tuple (ean, hsn_code, shelf_life) for the given row. If row has ean or hsn_code, use that. Otherwise, fetch from item_table using subcategory, brand, model, variant, color. """ ean = getattr(row, 'ean', '') or '' hsn = getattr(row, 'hsn_code', '') or '' shelf_life = getattr(row, 'shelf_life', '') or '' if not ean or not hsn or not shelf_life: item = item_table.objects.filter( sub_category_id=row.subcategory_id, brand_id=row.brand_id, model_id=row.model_id, variant_id=row.variant_id, color_id=row.color_id, status=1, is_active=1 ).first() if item: ean = item.ean or ean hsn = item.hsn_code or hsn shelf_life = item.cell_life or '' return ean, hsn, shelf_life from datetime import date from scheme.models import item_scheme_table from decimal import Decimal def get_imei_stock_summary_data(branch_id, is_ho, filters, include_schemes=False): """ Common logic to fetch and format IMEI stock summary data. Args: branch_id (int): The current branch ID. is_ho (int): 1 if HO, 0 otherwise. filters (dict): Dictionary containing filter keys: - subcategory, brand_id, model_id, variant_id, color_id - stock (filter for 'stock' or 'demo') - branch_id (filter for specific branch if HO) include_schemes (bool): Whether to calculate and include active scheme values. Returns: tuple: (available_qty, formatted_data_list) """ subcategory_id = filters.get('subcategory') brand_id = filters.get('brand_id') model_id = filters.get('model_id') variant_id = filters.get('variant_id') color_id = filters.get('color_id') stock_filter = filters.get('stock') branch_filter = filters.get('branch_id') # ✅ HO Override: Use filtered branch if selected if is_ho == 1 and branch_filter: branch_id = branch_filter formatted = [] counter = 1 available_qty = 0 processed_imei_set = set() # ------------------------- # SOLD / REMOVED IMEIS (Tracked per Branch) # ------------------------- # Base filter for sold items sold_filter = Q(is_active=1, status=1) # ------------------------- # NET STOCK TRACKING (Per Branch) # ------------------------- # 🔹 Use counters to track total Inwards and Outwards per Branch inwards_count = defaultdict(lambda: Counter()) outwards_count = defaultdict(lambda: Counter()) # Initial Filter (Active items) base_filter = Q(is_active=1, status=1) if is_ho == 1: if branch_filter: base_filter &= Q(branch_id=branch_filter) else: base_filter &= Q(branch_id=branch_id) # IDs of sales to wholesale customers wholesale_sales_ids = list(sales_order_table.objects.filter(customer_type__iexact='wholesale').values_list('id', flat=True)) # 1. OUTWARDS (Removals) # Sales: IMEIs sold to customers (Exclude Wholesale Initial Sales) sales_sold = child_sales_order_table.objects.filter(base_filter)\ .exclude(tm_sales_id__in=wholesale_sales_ids)\ .exclude(imei_no__isnull=True).exclude(imei_no='')\ .values('imei_no', 'branch_id') for r in sales_sold: outwards_count[r['branch_id']][r['imei_no']] += 1 # Wholesale Final Sold (tm_wholesale) wholesale_sold_imeis = set(wholesale_child_sales_order_table.objects.filter( status=1 ).exclude(imei_no__isnull=True).exclude(imei_no='').values_list('imei_no', flat=True)) # Wholesale Pending (Non Open Stock) pending_wholesale_imeis = set(child_sales_order_table.objects.filter( base_filter, tm_sales_id__in=wholesale_sales_ids ).exclude(imei_no__isnull=True).exclude(imei_no='').values_list('imei_no', flat=True)) # Purchase Return: IMEIs returned to supplier - Only count approved ones purchase_returns = child_purchase_return_table.objects.filter( base_filter, tm_return_id__in=purchase_return_table.objects.exclude( pr_status__iexact='reversed' ).values_list('id', flat=True) ).exclude(imei_no__isnull=True).exclude(imei_no='')\ .values('imei_no', 'branch_id') for r in purchase_returns: outwards_count[r['branch_id']][r['imei_no']] += 1 # Material Outward: IMEIs transferred out outwards = child_material_outward_table.objects.filter(base_filter)\ .exclude(tm_material_id__in=material_outward_table.objects.filter(outward_status='rejected').values_list('id', flat=True))\ .exclude(imei_no__isnull=True).exclude(imei_no='')\ .values('imei_no', 'branch_id') for r in outwards: outwards_count[r['branch_id']][r['imei_no']] += 1 # 2. INWARDS (Additions) stock_sources = [ ('Purchase', child_purchase_inward_table), ('Material In', child_material_inward_table), ('Opening Stock', opening_stock_table), ('Sales Return', child_sales_return_table) ] all_inward_rows = [] for source_name, model in stock_sources: # Apply filters (Category, Brand, Model, etc.) item_filters = base_filter if subcategory_id: item_filters &= Q(subcategory_id=subcategory_id) if brand_id: item_filters &= Q(brand_id=brand_id) if model_id: item_filters &= Q(model_id=model_id) if variant_id: item_filters &= Q(variant_id=variant_id) if color_id: item_filters &= Q(color_id=color_id) rows = model.objects.filter(item_filters)\ .exclude(imei_no__isnull=True).exclude(imei_no='') for row in rows: inwards_count[row.branch_id][row.imei_no] += 1 # Attach source name for later display row.source_type = source_name all_inward_rows.append(row) # ------------------------- # PROCESS INWARDS CHRONOLOGICALLY # ------------------------- # Sort all inward records by created_on DESC (Latest Inward Wins) all_inward_rows.sort(key=lambda x: x.created_on, reverse=True) def format_imei_row(row): nonlocal counter, available_qty # Unique key for this IMEI at this branch imei_branch_key = (row.imei_no, row.branch_id) if not row.imei_no or imei_branch_key in processed_imei_set: return # Check global wholesale sold if row.imei_no in wholesale_sold_imeis: return # 🔹 Net Stock Check net_at_branch = inwards_count[row.branch_id][row.imei_no] - outwards_count[row.branch_id][row.imei_no] if net_at_branch <= 0: return # Mark as processed so we only show the LATEST record for this IMEI-Branch pair processed_imei_set.add(imei_branch_key) # Check Non Open Stock is_non_open = row.imei_no in pending_wholesale_imeis # Apply stock / demo / non-open filter if stock_filter == 'non_open_stock': if not is_non_open: return elif stock_filter == 'stock': if is_non_open: return # Filter out Non Open Stock for strict 'stock' requests if getattr(row, 'is_demo', 0) == 1: return elif stock_filter == 'demo': if getattr(row, 'is_demo', 0) == 0: return elif stock_filter == 'all_except_non_open': if is_non_open: return # Allow both stock (is_demo=0) and demo (is_demo=1) elif filters.get('include_non_open'): # Default view (no filter selected) # Show STOCK + NON OPEN STOCK (Exclude DEMO) if getattr(row, 'is_demo', 0) == 1: return else: # Fallback (should ideally not happen if filters are controlled) if is_non_open: return if getattr(row, 'is_demo', 0) == 1: return # Stock status badge logic action_button = "" stock_badge = "" if is_non_open: stock_badge = 'Non Open Stock' action_button = '' elif getattr(row, 'is_demo', 0): stock_badge = 'Demo' action_button = ( f'' ) else: stock_badge = 'Stock' action_button = ( f'' ) # Get EAN / HSN ean, hsn, shelf_life = get_ean_hsn(row) available_qty += 1 # Price calculation fsp, wsp, mop, bop = get_final_price_for_imei( branch_id=row.branch_id, subcategory_id=row.subcategory_id, brand_id=row.brand_id, model_id=row.model_id, variant_id=row.variant_id, imei_no=row.imei_no, fsp=getattr(row, 'fsp', 0), wsp=getattr(row, 'wsp', 0), mop=getattr(row, 'mop', 0), bop=getattr(row, 'bop', 0) ) # Base MOP/BOP to Decimal mop = Decimal(str(mop)) if mop is not None else Decimal(0) bop = Decimal(str(bop)) if bop is not None else Decimal(0) # Rate and Amount rate = getattr(row, 'rate', 0) or getattr(row, 'db_price', 0) qty = float(getattr(row, 'quantity', 1)) amount = getattr(row, 'amount', 0) or (float(rate) * qty) db_price = getattr(row, 'db_price', 0) # Schemes scheme_mop = Decimal(0) scheme_bop = Decimal(0) if include_schemes and getattr(row, 'is_demo', 0) == 0: today = date.today() active_schemes = item_scheme_table.objects.filter( status=1, from_date__lte=today, to_date__gte=today, subcategory_id=row.subcategory_id, brand_id=row.brand_id, model_id=row.model_id, variant_id=row.variant_id ) for scheme in active_schemes: mop_drop = Decimal(str(scheme.mop_drop or 0)) bop_drop = Decimal(str(scheme.bop_drop or 0)) if scheme.scheme_type in ['upgrade', 'special']: mop += mop_drop bop += bop_drop scheme_mop += mop_drop scheme_bop += bop_drop if is_ho == 0: amount_qs = fsp else: if row.source_type == 'Material In': print('source_type',row.source_type) supply_branch = branch_table.objects.filter(id=getattr(row, 'branch_id',0)).first() print('supply_branch',supply_branch.is_ho) if supply_branch and supply_branch.is_ho == 1: purchase_row = child_purchase_inward_table.objects.filter(imei_no=row.imei_no, status=1).order_by('-created_on').first() print('purchase_row',purchase_row, row.imei_no) if purchase_row: amount_qs = purchase_row.amount else: opening_row = opening_stock_table.objects.filter(imei_no=row.imei_no, status=1).first() if opening_row: amount_qs = opening_row.db_price else: amount_qs = db_price else: amount_qs = db_price else: amount_qs = amount amount_qs = format_amount(amount_qs) formatted.append({ 'id': counter, 'action': action_button, 'branch': getItemNameById(branch_table, row.branch_id), 'sub_category': getItemNameById(sub_category_table, row.subcategory_id), 'brand': getItemNameById(brand_table, row.brand_id), 'model': getItemNameById(model_table, row.model_id), 'variant': getItemNameById(variant_table, row.variant_id), 'color': getItemNameById(color_table, row.color_id), 'shelf_life': shelf_life, 'imei': row.imei_no, 'ean': ean, 'hsn_code': hsn, 'source': row.source_type, 'fsp': format_amount(fsp), 'wsp': format_amount(wsp), 'mrp': format_amount(getattr(row, 'mrp', 0)), 'mop': format_amount(mop), 'bop': format_amount(bop), 'rate': format_amount(rate), 'amount': format_amount(amount_qs), 'db_price': format_amount(db_price), 'stock': 1, 'is_demo': getattr(row, 'is_demo', 0), 'demo_badge': stock_badge, 'scheme_mop': format_amount(scheme_mop), 'scheme_bop': format_amount(scheme_bop), }) counter += 1 # Format the rows for row in all_inward_rows: format_imei_row(row) return available_qty, formatted