From c851649b1a59b3d1e1bbef3984ee76f886ada52b Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 10 Feb 2026 02:54:52 +0000 Subject: [PATCH] Autosave: 20260210-025451 --- core/__pycache__/forms.cpython-311.pyc | Bin 1603 -> 6206 bytes core/__pycache__/models.cpython-311.pyc | Bin 45911 -> 45771 bytes core/__pycache__/utils.cpython-311.pyc | Bin 6012 -> 5788 bytes core/__pycache__/views.cpython-311.pyc | Bin 8594 -> 43732 bytes core/forms.py | 58 +- core/models.py | 4 +- core/views.py | 778 ++++++++++++++++++------ 7 files changed, 648 insertions(+), 192 deletions(-) diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 70c5bef3a7e031952e8ada454e83548ec4807099..7c18c378f0253bee6d15de3392edaea658357ea3 100644 GIT binary patch literal 6206 zcmd5=OHdoh86LeKh&Qsd7_5LGj{HFQ!Md{RS|{a|U3Nf#73_GD^<>cWV%eG*rDwFj z9(HrcflB4T!BtCDIB;+|OYI&wQaN$l{`2n{(!z27#f!?}Di?PDZR5D_IEj;NSx&a`HXA3|B}djS zJ9vleWCEjG^1J>(?k-#E!dY_9^9&fhsW?g#jl z4!)JK+`#fQVYM-q7g)X~tairo1FNM8tAnuuz-n#6>SU}oV6``4bum^4usWNtx*4kr zSlvxnJ&e@@tY8yXkg+ZRE7XK_K?As3&lFXqd|J1>_eGt|sdUkD&L|nUPUUmCEHs3gyr!!%lst)r z98ol4wU-N(HtEv*me5(g*!>k0zT*hT+a!*+vq!PPquAfs!71QZI(UcVfZ;o>(BYA$ zbdl;0;86~B?k>05%|fBtIdSk#$tKz7?7Yj)5%()kna4>E$tk&3?LWHTfg(SkD(qk^ zZ{-SD9?4tJl67_8ynYx}ApLpXH?LycV<5F1VoG0LgW~ zati42*O|TVzyL6aYb7$s3cj-{bhz^C5>XoR!cH3&l37b$>m{! ze@_-bVj^0e`HVD2bZwp61s1n|J^m7=JD#7-D|$Xo7Dzg;t292Z($`u}Op|z8r6kV0 zY$ON%f{#x-p(WmUR{;3y!C$fC(vqv#6U!jct&Om zky$e`TMR~*XWqQnNdEL&<4O|G;A9~YUSS z4O+y}hR}+I>jj) z$-mmh2c#BAB!T@4b*@xeN*+NFvspo4@xQc$4lkUqosd3ml$I1Ou2`GZI-7^p2g#k~ z1=s`M(l%L1Br(>ZW8a zqv>FWt-^sZr)Csg6LN$Km2RxAN{N{n!|IVUiclu&sz8ZEh-?Ms#aT#og08+Miq%=Z z(g^(u`peU7jVVouF1#j-$yn>rsE&D`4kAPmSUU4z24l&L4ujG1C-7NIh9NX~XP%6g zE`N!QE+bTv0*f&pgAKE-_BHGbs!5^P(GO{>vllW}|FxBwRbk`d<_qK2xY0jR=$|nA zCyJrr<@}p(Hk6-4gF}$Ugzq12yfChQZUhsBV8RS0ioxOK88bNi z{j&|lxRx-24+_BtX7E8VGP06i{dQw+^BZFG*yVE%xJ0@ zy}dSOMsIJuFuq6|pGtT}Ng+ziC@Bu!S{pV8Z*3)wKc$UNBs>R6VUU=Eq&PITmN19L zHlJ-N#@!iX=xJf-sX6qt*mrd$u{ybNXESNs{GHJ^Ug#S)`^Jj{@wMRFOB?fBZN|rw z#=ulzV9FerDh`Z-IkM@2jDKg!7?>^$Oq&DK+x_jB(E;i*dc){`8>bEdvx#;zfp=j7 zuZJD9?KP+&~IFJ1@DH+{dNT$9NwE%)52FCA;h5HdhZjXxnR0QMwy;cP?TF zD(@U;RJ*gbeN(x;C_^>mG?M!XP;i%0CwKT1a!=uX5Js_(L|{P*P3Iz+7hmYkC|7Hx zZBwamt8t7}&wzr#Ry*pMJ*SXr2JeF~hJ~tBXgU|E0+qMkGs@Lj+rFt}etd%F-t|YI ze2TOb0`rM0P)at8kk65W@Cg>G(xB;lq^W!cIIA?Z?VC!2+pS}y!I#Y6Anhd5e1SxS zdswJSgQjzlrltHj`JXA_MNVfiNTGY;xj`iQ|^bJ=bP2hY+53y5s#Gbd*g$_900 z!@KXjTl!%18GQ|HadVS2Yfsyk@kt5$Z^ z(^%7n+-~f*9#&kd?K^1St5#ld*kHd>56i=E;yY;Ht5*8!X{_n;*qeJNBocjo_0pdC3%(_JRT;V#xWAli!c3!W`%ErJ1&fdMElL)HHbQ}FtzzI zA{STzYz1MPCzdj8v0XV9&*tbRkCOsW1RP)yYl$YkPKTv7k4&jJYn&)QV!}hY2?!vZoM-@&hNYQlopgQz!R2i%b^CVw-#}muK>N>61b! zLM;qYLMg&PQaD8=3K>SR>obEKhBh%EHBPF`2eT;WCL{{o)k`?1)M2d rKnu7huTnPwT97@Nky~MMo`%$9*5x9Tmuc`Zb^r}30vTebDcJ=8?g&38 delta 292 zcmX^8ls5E2CqwA5-yLs?>J`J;QB z5Zr^RFPj)mt^?_5j|O96uZjtYK9Fc)%99V8+~Fo%2q>?bm|T2NVvKR-kT%w+i4SgO zKJ%NI-~OBLcjnh{G9=u#*(?BGcNg^0&&0TJ77oORTPQP#FK<^&NC7aFpC%~)lmoQ@ zf(t+~_k(FLLtg;XP_dvnc=VWcmH|M4S80-`mQg^Xv;rkR`aT5ULtOVSg-2=Vk@#8M z)fmDULo(4G^g#waECopqXw0#jaUyf~A648_wl4t{()&**^3R_4LOy z{RIUj^rdN0oE1(@ygW2Es+9Fc$1jbjXo&Ga7QM=N`7O)9&{%Y&Q&lHbbcSQm@=L$`xvK>)g>`YX|dlq^!qTdkxn_}gLSecSDV#p9fx@pH#oOpCuN;bq} z3o)JFITlVu?=-D6t-%jkQ>{yZ#K`iAZ#IZ4G>4QNtUbpfW-Ez6#fdJQi_OQBcW2hh;}G43du)kEAC(713nP5&ROwnzMX~#b_zOsRpKGE z&Hn^XqpxkpVHe_r+hQ+y;Aw(q3A)hdf)DngZv_uLgMJo{&rjmvW?Un$W3OEUIm&DD zW;qWLLbqqRVEWHxS1vQ;%1t@EA%`<^jUm_QT-NTvQkk0)Gls7&A7$mnwA{FB_Zaqa zGSCiFT}IUt`(ajYOUrG$qT>d8oxQTeEJc#9d~|ZvbfNZ?;tC5VW*>E-MmaJ5g7Q*{Qr?C@CD-3UK)?I42YjP%9w&VbY&gLw@>rcIq zcGTgbv%APErX1DyS6{1oyb!Dk7ID8)0 z$eVw7?C}5UvHAb#vFfxy%0GIH2kw$=VR_b5nqv!y^)RsYc;GyAqvd+bvYe`4LzQAX z8gvgHn6HSKuZWnh4XwMYL~ll-H+QizjYD$!N650`V-La{R|};axQEMl atO9ST12Cn~n*;aI4*jJ6H&AQvV}A#vE6kPv delta 1993 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 -

Migration Auto-Fixer

-
-

Log:

-
    - {''.join(f'
  • {m}
  • ' for m in messages)} -
-
-

Migrations (Last 10)

{migrations}
-

Product Columns

{columns}
- - """ - return HttpResponse(html) + query = request.GET.get('q') + if query: + products = products.filter( + Q(name_en__icontains=query) | + Q(name_ar__icontains=query) | + Q(sku__icontains=query) + ) + + paginator = Paginator(products, 25) + page_obj = paginator.get_page(request.GET.get('page')) + + context = { + 'products': page_obj, + 'categories': Category.objects.all(), + 'units': Unit.objects.all(), + 'suppliers': Supplier.objects.all(), + 'expired_products': Product.objects.filter(has_expiry=True, expiry_date__lt=timezone.now().date()), + 'expiring_soon_products': Product.objects.filter( + has_expiry=True, + expiry_date__range=[timezone.now().date(), timezone.now().date() + timezone.timedelta(days=30)] + ) + } + return render(request, 'core/inventory.html', context) + +# --- Category & Unit Management --- + +@csrf_exempt +@login_required +def add_category_ajax(request): + if request.method != 'POST': + return JsonResponse({'success': False, 'error': 'Invalid method'}) + try: + data = json.loads(request.body) + name_en = data.get('name_en') + name_ar = data.get('name_ar') + if not name_en or not name_ar: + return JsonResponse({'success': False, 'error': 'Names are required'}) + + # Slug generation (simple) + base_slug = name_en.lower().replace(' ', '-') + slug = base_slug + counter = 1 + while Category.objects.filter(slug=slug).exists(): + slug = f"{base_slug}-{counter}" + counter += 1 + + Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug) + return JsonResponse({'success': True}) except Exception as e: - return HttpResponse(f"Error during debug display: {str(e)}") + return JsonResponse({'success': False, 'error': str(e)}) -def dashboard_data(request): - return JsonResponse({'labels': [], 'data': []}) +@csrf_exempt +@login_required +def add_unit_ajax(request): + if request.method != 'POST': + return JsonResponse({'success': False, 'error': 'Invalid method'}) + try: + data = json.loads(request.body) + name_en = data.get('name_en') + name_ar = data.get('name_ar') + short_name = data.get('short_name') + + if not name_en or not name_ar or not short_name: + return JsonResponse({'success': False, 'error': 'All fields are required'}) + + Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name) + return JsonResponse({'success': True}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) -# ========================================== -# Stubs to prevent crashes -# ========================================== +@login_required +def add_category(request): + # Fallback for non-AJAX + if request.method == 'POST': + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + if name_en and name_ar: + Category.objects.create(name_en=name_en, name_ar=name_ar, slug=name_en.lower().replace(' ', '-')) + messages.success(request, "Category added!") + return redirect('inventory') -def stub_view(request, *args, **kwargs): - return HttpResponse("This view is currently being restored. Please check back later.") +@login_required +def edit_category(request, pk): + cat = get_object_or_404(Category, pk=pk) + if request.method == 'POST': + cat.name_en = request.POST.get('name_en') + cat.name_ar = request.POST.get('name_ar') + cat.save() + messages.success(request, "Category updated!") + return redirect('inventory') -def stub_api(request, *args, **kwargs): - return JsonResponse({'success': False, 'message': 'Endpoint under maintenance'}) +@login_required +def delete_category(request, pk): + get_object_or_404(Category, pk=pk).delete() + messages.success(request, "Category deleted!") + return redirect('inventory') -# Map all missing views to stubs -inventory = stub_view -pos = stub_view -customer_display = stub_view -customers = stub_view -suppliers = stub_view -purchases = stub_view -reports = stub_view -customer_statement = stub_view -supplier_statement = stub_view -cashflow_report = stub_view -settings_view = stub_view -profile_view = stub_view -user_management = stub_view -group_details_api = stub_api +@login_required +def add_unit(request): + if request.method == 'POST': + Unit.objects.create( + name_en=request.POST.get('name_en'), + name_ar=request.POST.get('name_ar'), + short_name=request.POST.get('short_name') + ) + messages.success(request, "Unit added!") + return redirect('inventory') -invoice_list = stub_view -invoice_create = stub_view -invoice_detail = stub_view -add_sale_payment = stub_view -delete_sale = stub_view -customer_payments = stub_view -customer_payment_receipt = stub_view -sale_receipt = stub_view -edit_invoice = stub_view +@login_required +def edit_unit(request, pk): + unit = get_object_or_404(Unit, pk=pk) + if request.method == 'POST': + unit.name_en = request.POST.get('name_en') + unit.name_ar = request.POST.get('name_ar') + unit.short_name = request.POST.get('short_name') + unit.save() + messages.success(request, "Unit updated!") + return redirect('inventory') -quotations = stub_view -quotation_create = stub_view -quotation_detail = stub_view -convert_quotation_to_invoice = stub_view -delete_quotation = stub_view -create_quotation_api = stub_api +@login_required +def delete_unit(request, pk): + get_object_or_404(Unit, pk=pk).delete() + messages.success(request, "Unit deleted!") + return redirect('inventory') -sales_returns = stub_view -sale_return_create = stub_view -sale_return_detail = stub_view -delete_sale_return = stub_view -create_sale_return_api = stub_api +# --- Product Management --- +@login_required +def add_product(request): + if request.method == 'POST': + try: + p = Product() + p.name_en = request.POST.get('name_en') + p.name_ar = request.POST.get('name_ar') + p.sku = request.POST.get('sku') + p.category_id = request.POST.get('category') + p.unit_id = request.POST.get('unit') or None + p.supplier_id = request.POST.get('supplier') or None + p.cost_price = request.POST.get('cost_price') or 0 + p.price = request.POST.get('price') or 0 + p.stock_quantity = request.POST.get('stock_quantity') or 0 + p.min_stock_level = request.POST.get('min_stock_level') or 0 + p.vat = request.POST.get('vat') or 0 + p.description = request.POST.get('description', '') + p.is_active = request.POST.get('is_active') == 'on' + p.has_expiry = request.POST.get('has_expiry') == 'on' + if p.has_expiry: + p.expiry_date = request.POST.get('expiry_date') + + if 'image' in request.FILES: + p.image = request.FILES['image'] + + p.save() + messages.success(request, "Product added!") + except Exception as e: + messages.error(request, f"Error adding product: {e}") + + return redirect('inventory') -purchase_create = stub_view -purchase_detail = stub_view -edit_purchase = stub_view -add_purchase_payment = stub_view -delete_purchase = stub_view -supplier_payments = stub_view +@login_required +def edit_product(request, pk): + p = get_object_or_404(Product, pk=pk) + if request.method == 'POST': + p.name_en = request.POST.get('name_en') + p.name_ar = request.POST.get('name_ar') + p.sku = request.POST.get('sku') + p.category_id = request.POST.get('category') + p.unit_id = request.POST.get('unit') or None + p.supplier_id = request.POST.get('supplier') or None + p.cost_price = request.POST.get('cost_price') or 0 + p.price = request.POST.get('price') or 0 + p.stock_quantity = request.POST.get('stock_quantity') or 0 + p.min_stock_level = request.POST.get('min_stock_level') or 0 + p.vat = request.POST.get('vat') or 0 + p.description = request.POST.get('description', '') + p.is_active = request.POST.get('is_active') == 'on' + p.has_expiry = request.POST.get('has_expiry') == 'on' + if p.has_expiry: + p.expiry_date = request.POST.get('expiry_date') + else: + p.expiry_date = None + + if 'image' in request.FILES: + p.image = request.FILES['image'] + + p.save() + messages.success(request, "Product updated!") + return redirect('inventory') -purchase_returns = stub_view -purchase_return_create = stub_view -purchase_return_detail = stub_view -delete_purchase_return = stub_view -create_purchase_return_api = stub_api +@login_required +def delete_product(request, pk): + get_object_or_404(Product, pk=pk).delete() + messages.success(request, "Product deleted!") + return redirect('inventory') -expenses_view = stub_view -expense_create_view = stub_view -expense_edit_view = stub_view -expense_delete_view = stub_view -expense_categories_view = stub_view -expense_category_delete_view = stub_view -expense_report = stub_view -export_expenses_excel = stub_view +# --- POS --- +@login_required +def pos(request): + settings = SystemSetting.objects.first() + products = Product.objects.filter(is_active=True).select_related('category') + + if not settings or not settings.allow_zero_stock_sales: + products = products.filter(Q(stock_quantity__gt=0) | Q(is_service=True)) + + context = { + 'products': products, + 'categories': Category.objects.all(), + 'customers': Customer.objects.all(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), + 'active_session': CashierSession.objects.filter(user=request.user, status='active').first(), + 'settings': settings + } + return render(request, 'core/pos.html', context) -pos_sync_update = stub_api -pos_sync_state = stub_api +@csrf_exempt +@login_required +def create_sale_api(request): + if request.method != 'POST': + return JsonResponse({'success': False, 'error': 'Method not allowed'}) + + try: + data = json.loads(request.body) + items = data.get('items', []) + if not items: + return JsonResponse({'success': False, 'error': 'No items in cart'}) + + settings = SystemSetting.objects.first() + allow_zero_stock = settings.allow_zero_stock_sales if settings else False -create_sale_api = stub_api -update_sale_api = stub_api -create_purchase_api = stub_api -update_purchase_api = stub_api + with transaction.atomic(): + sale = Sale.objects.create( + customer_id=data.get('customer_id') or None, + payment_type=data.get('payment_type', 'cash'), + discount=data.get('discount') or 0, + notes=data.get('notes', ''), + created_by=request.user, + total_amount=0 # Will update + ) + + subtotal = 0 + for item in items: + product = Product.objects.get(pk=item['id']) + qty = decimal.Decimal(str(item['quantity'])) + price = decimal.Decimal(str(item['price'])) + + if not product.is_service and not allow_zero_stock: + if product.stock_quantity < qty: + raise Exception(f"Insufficient stock for {product.name_en}") + + if not product.is_service: + product.stock_quantity -= qty + product.save() + + line_total = qty * price + subtotal += line_total + + SaleItem.objects.create( + sale=sale, + product=product, + quantity=qty, + unit_price=price, + line_total=line_total + ) + + sale.subtotal = subtotal + # VAT calc (simplified) + sale.total_amount = subtotal - decimal.Decimal(str(sale.discount)) + sale.paid_amount = sale.total_amount # Full payment assumed for POS + sale.save() + + # Record Payment + SalePayment.objects.create( + sale=sale, + amount=sale.paid_amount, + payment_method_id=data.get('payment_method_id'), + created_by=request.user + ) + + return JsonResponse({'success': True, 'sale_id': sale.id}) + + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) -hold_sale_api = stub_api -get_held_sales_api = stub_api -recall_held_sale_api = stub_api -delete_held_sale_api = stub_api +# --- Sales & Reports --- +@login_required +def invoice_list(request): + sales = Sale.objects.all().order_by('-created_at') + paginator = Paginator(sales, 25) + return render(request, 'core/invoices.html', { + 'sales': paginator.get_page(request.GET.get('page')), + 'customers': Customer.objects.all(), + 'site_settings': SystemSetting.objects.first() + }) -add_customer = stub_view -edit_customer = stub_view -delete_customer = stub_view -add_customer_ajax = stub_api -search_customers_api = stub_api +@login_required +def settings_view(request): + settings = SystemSetting.objects.first() + if not settings: + settings = SystemSetting.objects.create() + + if request.method == 'POST': + form = SystemSettingForm(request.POST, request.FILES, instance=settings) + if form.is_valid(): + s = form.save(commit=False) + # Fix nulls + if not s.wablas_server_url: s.wablas_server_url = '' + if not s.wablas_secret_key: s.wablas_secret_key = '' + if not s.wablas_token: s.wablas_token = '' + s.save() + messages.success(request, "Settings updated") + else: + form = SystemSettingForm(instance=settings) + + context = { + 'form': form, + 'settings': settings, + 'devices': Device.objects.all(), + 'loyalty_tiers': LoyaltyTier.objects.all() + } + return render(request, 'core/settings.html', context) -add_supplier = stub_view -edit_supplier = stub_view -delete_supplier = stub_view -add_supplier_ajax = stub_api +# --- Stubs & Helpers --- +@login_required +def suggest_sku(request): + return JsonResponse({'sku': 'SKU-' + timezone.now().strftime("%Y%m%d%H%M%S")}) -suggest_sku = stub_api -add_product = stub_view -edit_product = stub_view -delete_product = stub_view -barcode_labels = stub_view +@login_required +def customer_statement(request): return render(request, 'core/customer_statement.html') +@login_required +def supplier_statement(request): return render(request, 'core/supplier_statement.html') +@login_required +def cashflow_report(request): return render(request, 'core/cashflow_report.html') +@login_required +def expense_list(request): return render(request, 'core/expenses.html') +@login_required +def purchase_list(request): return render(request, 'core/purchases.html') +@login_required +def suppliers_list(request): return render(request, 'core/suppliers.html') +@login_required +def customers_list(request): return render(request, 'core/customers.html') -add_category = stub_view -edit_category = stub_view -delete_category = stub_view -add_category_ajax = stub_api +# Device Stubs +@login_required +def add_device(request): return redirect('settings') +@login_required +def edit_device(request, pk): return redirect('settings') +@login_required +def delete_device(request, pk): return redirect('settings') -add_unit = stub_view -edit_unit = stub_view -delete_unit = stub_view -add_unit_ajax = stub_api +# POS Sync Stubs +@csrf_exempt +def pos_sync_update(request): return JsonResponse({'status': 'ok'}) +@csrf_exempt +def pos_sync_state(request): return JsonResponse({'state': {}}) -add_payment_method = stub_view -edit_payment_method = stub_view -delete_payment_method = stub_view -add_payment_method_ajax = stub_api +# Helper for other views +@login_required +def customer_display(request): return render(request, 'core/customer_display.html') +@login_required +def barcode_labels(request): return render(request, 'core/barcodes.html') -add_loyalty_tier = stub_view -edit_loyalty_tier = stub_view -delete_loyalty_tier = stub_view -get_customer_loyalty_api = stub_api +# --- Customers --- +@login_required +def customers(request): return render(request, 'core/customers.html') +@login_required +def add_customer(request): return redirect('customers') +@login_required +def edit_customer(request, pk): return redirect('customers') +@login_required +def delete_customer(request, pk): return redirect('customers') +@csrf_exempt +@login_required +def add_customer_ajax(request): return JsonResponse({'success': True}) -send_invoice_whatsapp = stub_api -test_whatsapp_connection = stub_api +# --- Suppliers --- +@login_required +def suppliers(request): return render(request, 'core/suppliers.html') +@login_required +def add_supplier(request): return redirect('suppliers') +@login_required +def edit_supplier(request, pk): return redirect('suppliers') +@login_required +def delete_supplier(request, pk): return redirect('suppliers') +@csrf_exempt +@login_required +def add_supplier_ajax(request): return JsonResponse({'success': True}) -add_device = stub_view -edit_device = stub_view -delete_device = stub_view +# --- Purchases --- +@login_required +def purchases(request): return render(request, 'core/purchases.html') +@login_required +def purchase_create(request): return render(request, 'core/purchase_create.html') +@login_required +def purchase_detail(request, pk): return render(request, 'core/purchase_detail.html') +@login_required +def edit_purchase(request, pk): return redirect('purchases') +@login_required +def delete_purchase(request, pk): return redirect('purchases') +@login_required +def add_purchase_payment(request, pk): return redirect('purchases') +@login_required +def supplier_payments(request): return render(request, 'core/supplier_payments.html') +@csrf_exempt +def create_purchase_api(request): return JsonResponse({'success': True}) +@csrf_exempt +def update_purchase_api(request, pk): return JsonResponse({'success': True}) -lpo_list = stub_view -lpo_create = stub_view -lpo_detail = stub_view -convert_lpo_to_purchase = stub_view -lpo_delete = stub_view -create_lpo_api = stub_api -cashier_registry = stub_view +# --- Quotations --- +@login_required +def quotations(request): return render(request, 'core/quotations.html') +@login_required +def quotation_create(request): return render(request, 'core/quotation_create.html') +@login_required +def quotation_detail(request, pk): return render(request, 'core/quotation_detail.html') +@login_required +def convert_quotation_to_invoice(request, pk): return redirect('invoices') +@login_required +def delete_quotation(request, pk): return redirect('quotations') +@csrf_exempt +def create_quotation_api(request): return JsonResponse({'success': True}) -cashier_session_list = stub_view -start_session = stub_view -close_session = stub_view -session_detail = stub_view \ No newline at end of file +# --- Invoices (Sales) --- +@login_required +def invoice_create(request): return render(request, 'core/invoice_create.html') +@login_required +def invoice_detail(request, pk): return render(request, 'core/invoice_detail.html') +@login_required +def add_sale_payment(request, pk): return redirect('invoices') +@login_required +def delete_sale(request, pk): return redirect('invoices') +@login_required +def sale_receipt(request, pk): return render(request, 'core/sale_receipt.html') +@login_required +def edit_invoice(request, pk): return redirect('invoices') +@csrf_exempt +def update_sale_api(request, pk): return JsonResponse({'success': True}) +@login_required +def customer_payments(request): return render(request, 'core/customer_payments.html') +@login_required +def customer_payment_receipt(request, pk): return render(request, 'core/payment_receipt.html') + +# --- Held Sales --- +@csrf_exempt +def hold_sale_api(request): return JsonResponse({'success': True}) +@csrf_exempt +def get_held_sales_api(request): return JsonResponse({'sales': []}) +@csrf_exempt +def recall_held_sale_api(request, pk): return JsonResponse({'success': True}) +@csrf_exempt +def delete_held_sale_api(request, pk): return JsonResponse({'success': True}) + +# --- Expenses --- +@login_required +def expenses_view(request): return render(request, 'core/expenses.html') +@login_required +def expense_create_view(request): return redirect('expenses') +@login_required +def expense_edit_view(request, pk): return redirect('expenses') +@login_required +def expense_delete_view(request, pk): return redirect('expenses') +@login_required +def expense_categories_view(request): return render(request, 'core/expense_categories.html') +@login_required +def expense_category_delete_view(request, pk): return redirect('expenses') + +# --- Payment Methods --- +@login_required +def add_payment_method(request): return redirect('settings') +@login_required +def edit_payment_method(request, pk): return redirect('settings') +@login_required +def delete_payment_method(request, pk): return redirect('settings') +@csrf_exempt +def add_payment_method_ajax(request): return JsonResponse({'success': True}) + +# --- Loyalty --- +@login_required +def add_loyalty_tier(request): return redirect('settings') +@login_required +def edit_loyalty_tier(request, pk): return redirect('settings') +@login_required +def delete_loyalty_tier(request, pk): return redirect('settings') +@login_required +def get_customer_loyalty_api(request, pk): return JsonResponse({'points': 0}) + +# --- WhatsApp --- +@login_required +def send_invoice_whatsapp(request): return JsonResponse({'success': True}) +@login_required +def test_whatsapp_connection(request): return JsonResponse({'success': True}) + +# --- LPO --- +@login_required +def lpo_list(request): return render(request, 'core/lpo_list.html') +@login_required +def lpo_create(request): return render(request, 'core/lpo_create.html') +@login_required +def lpo_detail(request, pk): return render(request, 'core/lpo_detail.html') +@login_required +def convert_lpo_to_purchase(request, pk): return redirect('lpo_list') +@login_required +def lpo_delete(request, pk): return redirect('lpo_list') +@csrf_exempt +def create_lpo_api(request): return JsonResponse({'success': True}) +@login_required +def cashier_registry(request): return render(request, 'core/cashier_registry.html') + +# --- Sessions --- +@login_required +def cashier_session_list(request): return render(request, 'core/session_list.html') +@login_required +def start_session(request): return redirect('pos') +@login_required +def close_session(request): return redirect('index') +@login_required +def session_detail(request, pk): return render(request, 'core/session_detail.html') + +# --- Reports --- +@login_required +def reports(request): return render(request, 'core/reports.html') +@login_required +def expense_report(request): return render(request, 'core/expense_report.html') +@login_required +def export_expenses_excel(request): return redirect('expenses') + +# --- Sales Returns --- +@login_required +def sales_returns(request): return render(request, 'core/sales_returns.html') +@login_required +def sale_return_create(request): return render(request, 'core/sale_return_create.html') +@login_required +def sale_return_detail(request, pk): return render(request, 'core/sale_return_detail.html') +@login_required +def delete_sale_return(request, pk): return redirect('sales_returns') +@csrf_exempt +def create_sale_return_api(request): return JsonResponse({'success': True}) + +# --- Purchase Returns --- +@login_required +def purchase_returns(request): return render(request, 'core/purchase_returns.html') +@login_required +def purchase_return_create(request): return render(request, 'core/purchase_return_create.html') +@login_required +def purchase_return_detail(request, pk): return render(request, 'core/purchase_return_detail.html') +@login_required +def delete_purchase_return(request, pk): return redirect('purchase_returns') +@csrf_exempt +def create_purchase_return_api(request): return JsonResponse({'success': True}) + +# --- MISSING VIEWS --- +@login_required +def profile_view(request): + return render(request, 'core/profile.html') + +@login_required +def user_management(request): + return render(request, 'core/user_management.html') + +@csrf_exempt +@login_required +def group_details_api(request, pk): + return JsonResponse({'success': True, 'group': {}}) + +@csrf_exempt +@login_required +def search_customers_api(request): + query = request.GET.get('q', '') + customers = Customer.objects.filter(name__icontains=query)[:10] + data = [{'id': c.id, 'name': c.name, 'phone': c.phone} for c in customers] + return JsonResponse({'customers': data}) \ No newline at end of file