38086-vm/core/views.py
2026-02-02 17:09:26 +00:00

1532 lines
57 KiB
Python

import decimal
from django.contrib.auth.models import User, Group, Permission
from django.urls import reverse
import random
import string
from django.shortcuts import render, get_object_or_404, redirect
from django.db.models import Sum, Count, F
from django.db.models.functions import TruncDate, TruncMonth
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from .models import (
Product, Sale, Category, Unit, Customer, Supplier,
Purchase, PurchaseItem, PurchasePayment,
SaleItem, SalePayment, SystemSetting,
Quotation, QuotationItem,
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem,
PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction
)
import json
from datetime import timedelta
from django.utils import timezone
from django.contrib import messages
from django.utils.text import slugify
import openpyxl
@login_required
def index(request):
"""
Enhanced Meezan Dashboard View
"""
# Summary Stats
total_products = Product.objects.count()
total_sales_count = Sale.objects.count()
total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0
total_customers = Customer.objects.count()
# Stock Alert (Low stock < 5)
low_stock_products = Product.objects.filter(stock_quantity__lt=5)
# Recent Transactions
recent_sales = Sale.objects.order_by('-created_at').select_related('created_by')[:5]
# Chart Data: Sales for the last 7 days
seven_days_ago = timezone.now().date() - timedelta(days=6)
sales_over_time = Sale.objects.filter(created_at__date__gte=seven_days_ago) \
.annotate(date=TruncDate('created_at')) \
.values('date') \
.annotate(total=Sum('total_amount')) \
.order_by('date')
# Prepare data for Chart.js
chart_labels = []
chart_data = []
date_dict = {s['date']: float(s['total']) for s in sales_over_time}
for i in range(7):
date = seven_days_ago + timedelta(days=i)
chart_labels.append(date.strftime('%b %d'))
chart_data.append(date_dict.get(date, 0))
context = {
'total_products': total_products,
'total_sales_count': total_sales_count,
'total_sales_amount': total_sales_amount,
'total_customers': total_customers,
'low_stock_products': low_stock_products,
'recent_sales': recent_sales,
'chart_labels': json.dumps(chart_labels),
'chart_data': json.dumps(chart_data),
}
return render(request, 'core/index.html', context)
@login_required
def inventory(request):
products = Product.objects.all().select_related('category', 'unit', 'supplier')
categories = Category.objects.all()
suppliers = Supplier.objects.all()
units = Unit.objects.all()
context = {
'products': products,
'categories': categories,
'suppliers': suppliers,
'units': units
}
return render(request, 'core/inventory.html', context)
@login_required
def pos(request):
products = Product.objects.all().filter(stock_quantity__gt=0, is_active=True)
customers = Customer.objects.all()
categories = Category.objects.all()
payment_methods = PaymentMethod.objects.filter(is_active=True)
settings = SystemSetting.objects.first()
# Ensure at least Cash exists
if not payment_methods.exists():
PaymentMethod.objects.create(name_en="Cash", name_ar="نقدي", is_active=True)
payment_methods = PaymentMethod.objects.filter(is_active=True)
context = {
'products': products,
'customers': customers,
'categories': categories,
'payment_methods': payment_methods,
'settings': settings
}
return render(request, 'core/pos.html', context)
@login_required
def customers(request):
customers_list = Customer.objects.all().annotate(total_sales=Sum('sales__total_amount'))
context = {'customers': customers_list}
return render(request, 'core/customers.html', context)
@login_required
def suppliers(request):
suppliers_list = Supplier.objects.all()
context = {'suppliers': suppliers_list}
return render(request, 'core/suppliers.html', context)
# --- Purchase Views ---
@login_required
def purchases(request):
purchases_list = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at')
suppliers_list = Supplier.objects.all()
payment_methods = PaymentMethod.objects.filter(is_active=True)
context = {
'purchases': purchases_list,
'suppliers': suppliers_list,
'payment_methods': payment_methods
}
return render(request, 'core/purchases.html', context)
@login_required
def purchase_create(request):
products = Product.objects.filter(is_active=True)
suppliers = Supplier.objects.all()
payment_methods = PaymentMethod.objects.filter(is_active=True)
return render(request, 'core/purchase_create.html', {
'products': products,
'suppliers': suppliers,
'payment_methods': payment_methods
})
@login_required
def purchase_detail(request, pk):
purchase = get_object_or_404(Purchase, pk=pk)
settings = SystemSetting.objects.first()
return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': settings})
@csrf_exempt
@login_required
def create_purchase_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
supplier_id = data.get('supplier_id')
invoice_number = data.get('invoice_number', '')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
paid_amount = data.get('paid_amount', 0)
payment_type = data.get('payment_type', 'cash')
payment_method_id = data.get('payment_method_id')
due_date = data.get('due_date')
notes = data.get('notes', '')
supplier = None
if supplier_id:
supplier = Supplier.objects.get(id=supplier_id)
purchase = Purchase.objects.create(
supplier=supplier,
invoice_number=invoice_number,
total_amount=total_amount,
paid_amount=paid_amount,
balance_due=float(total_amount) - float(paid_amount),
payment_type=payment_type,
due_date=due_date if due_date else None,
notes=notes,
created_by=request.user
)
# Set status based on payments
if float(paid_amount) >= float(total_amount):
purchase.status = 'paid'
elif float(paid_amount) > 0:
purchase.status = 'partial'
else:
purchase.status = 'unpaid'
purchase.save()
# Record the initial payment if any
if float(paid_amount) > 0:
pm = None
if payment_method_id:
pm = PaymentMethod.objects.filter(id=payment_method_id).first()
PurchasePayment.objects.create(
purchase=purchase,
amount=paid_amount,
payment_method=pm,
payment_method_name=pm.name_en if pm else payment_type.capitalize(),
notes="Initial payment",
created_by=request.user
)
for item in items:
product = Product.objects.get(id=item['id'])
PurchaseItem.objects.create(
purchase=purchase,
product=product,
quantity=item['quantity'],
cost_price=item['price'],
line_total=item['line_total']
)
# Update Stock
product.stock_quantity += int(item['quantity'])
product.cost_price = item['price']
product.save()
return JsonResponse({'success': True, 'purchase_id': purchase.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def add_purchase_payment(request, pk):
purchase = get_object_or_404(Purchase, pk=pk)
if request.method == 'POST':
amount = request.POST.get('amount')
payment_date = request.POST.get('payment_date', timezone.now().date())
payment_method_id = request.POST.get('payment_method_id')
notes = request.POST.get('notes', '')
pm = None
if payment_method_id:
pm = PaymentMethod.objects.filter(id=payment_method_id).first()
PurchasePayment.objects.create(
purchase=purchase,
amount=amount,
payment_date=payment_date,
payment_method=pm,
payment_method_name=pm.name_en if pm else "Cash",
notes=notes,
created_by=request.user
)
purchase.update_balance()
messages.success(request, "Payment added successfully!")
return redirect('purchases')
@login_required
def delete_purchase(request, pk):
purchase = get_object_or_404(Purchase, pk=pk)
for item in purchase.items.all():
item.product.stock_quantity -= item.quantity
item.product.save()
purchase.delete()
messages.success(request, "Purchase deleted successfully!")
return redirect('purchases')
# --- Sale Views ---
@login_required
def invoice_list(request):
sales = Sale.objects.all().select_related('customer', 'created_by').order_by('-created_at')
customers = Customer.objects.all()
payment_methods = PaymentMethod.objects.filter(is_active=True)
return render(request, 'core/invoices.html', {
'sales': sales,
'customers': customers,
'payment_methods': payment_methods
})
@login_required
def invoice_create(request):
products = Product.objects.filter(is_active=True)
customers = Customer.objects.all()
payment_methods = PaymentMethod.objects.filter(is_active=True)
return render(request, 'core/invoice_create.html', {
'products': products,
'customers': customers,
'payment_methods': payment_methods
})
@login_required
def invoice_detail(request, pk):
sale = get_object_or_404(Sale, pk=pk)
settings = SystemSetting.objects.first()
return render(request, 'core/invoice_detail.html', {'sale': sale, 'settings': settings})
@csrf_exempt
@login_required
def create_sale_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
customer_id = data.get('customer_id')
invoice_number = data.get('invoice_number', '')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
paid_amount = data.get('paid_amount', 0)
discount = data.get('discount', 0)
payment_type = data.get('payment_type', 'cash')
payment_method_id = data.get('payment_method_id')
due_date = data.get('due_date')
notes = data.get('notes', '')
# Loyalty data
points_to_redeem = data.get('loyalty_points_redeemed', 0)
customer = None
if customer_id:
customer = Customer.objects.get(id=customer_id)
settings = SystemSetting.objects.first()
if not settings:
settings = SystemSetting.objects.create()
loyalty_discount = 0
if settings.loyalty_enabled and customer and points_to_redeem > 0:
if customer.loyalty_points >= points_to_redeem:
loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point)
sale = Sale.objects.create(
customer=customer,
invoice_number=invoice_number,
total_amount=total_amount,
paid_amount=paid_amount,
balance_due=float(total_amount) - float(paid_amount),
discount=discount,
loyalty_points_redeemed=points_to_redeem,
loyalty_discount_amount=loyalty_discount,
payment_type=payment_type,
due_date=due_date if due_date else None,
notes=notes,
created_by=request.user
)
# Set status based on payments
if float(paid_amount) >= float(total_amount):
sale.status = 'paid'
elif float(paid_amount) > 0:
sale.status = 'partial'
else:
sale.status = 'unpaid'
sale.save()
# Record initial payment if any
if float(paid_amount) > 0:
pm = None
if payment_method_id:
pm = PaymentMethod.objects.filter(id=payment_method_id).first()
SalePayment.objects.create(
sale=sale,
amount=paid_amount,
payment_method=pm,
payment_method_name=pm.name_en if pm else payment_type.capitalize(),
notes="Initial payment",
created_by=request.user
)
for item in items:
product = Product.objects.get(id=item['id'])
SaleItem.objects.create(
sale=sale,
product=product,
quantity=item['quantity'],
unit_price=item['price'],
line_total=item['line_total']
)
product.stock_quantity -= int(item['quantity'])
product.save()
# Handle Loyalty Points
if settings.loyalty_enabled and customer:
# Earn Points
points_earned = float(total_amount) * float(settings.points_per_currency)
if customer.loyalty_tier:
points_earned *= float(customer.loyalty_tier.point_multiplier)
if points_earned > 0:
customer.loyalty_points += decimal.Decimal(str(points_earned))
LoyaltyTransaction.objects.create(
customer=customer,
sale=sale,
transaction_type='earned',
points=points_earned,
notes=f"Points earned from Sale #{sale.id}"
)
# Redeem Points
if points_to_redeem > 0:
customer.loyalty_points -= decimal.Decimal(str(points_to_redeem))
LoyaltyTransaction.objects.create(
customer=customer,
sale=sale,
transaction_type='redeemed',
points=-points_to_redeem,
notes=f"Points redeemed for Sale #{sale.id}"
)
customer.update_tier()
customer.save()
return JsonResponse({
'success': True,
'sale_id': sale.id,
'business': {
'name': settings.business_name,
'address': settings.address,
'phone': settings.phone,
'email': settings.email,
'currency': settings.currency_symbol,
'vat_number': settings.vat_number,
'registration_number': settings.registration_number,
'logo_url': settings.logo.url if settings.logo else None
},
'sale': {
'id': sale.id,
'invoice_number': sale.invoice_number,
'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"),
'total': float(sale.total_amount),
'paid': float(sale.paid_amount),
'balance': float(sale.balance_due),
'items': [
{
'name_en': si.product.name_en,
'name_ar': si.product.name_ar,
'qty': si.quantity,
'price': float(si.unit_price),
'total': float(si.line_total)
} for si in sale.items.all()
]
}
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def add_sale_payment(request, pk):
sale = get_object_or_404(Sale, pk=pk)
if request.method == 'POST':
amount = request.POST.get('amount')
payment_date = request.POST.get('payment_date', timezone.now().date())
payment_method_id = request.POST.get('payment_method_id')
notes = request.POST.get('notes', '')
pm = None
if payment_method_id:
pm = PaymentMethod.objects.filter(id=payment_method_id).first()
SalePayment.objects.create(
sale=sale,
amount=amount,
payment_date=payment_date,
payment_method=pm,
payment_method_name=pm.name_en if pm else "Cash",
notes=notes,
created_by=request.user
)
sale.update_balance()
messages.success(request, "Payment added successfully!")
return redirect('invoices')
@login_required
def delete_sale(request, pk):
sale = get_object_or_404(Sale, pk=pk)
for item in sale.items.all():
item.product.stock_quantity += item.quantity
item.product.save()
sale.delete()
messages.success(request, "Sale deleted successfully!")
return redirect('invoices')
# --- Quotation Views ---
@login_required
def quotations(request):
quotations_list = Quotation.objects.all().select_related('customer', 'created_by').order_by('-created_at')
customers = Customer.objects.all()
return render(request, 'core/quotations.html', {'quotations': quotations_list, 'customers': customers})
@login_required
def quotation_create(request):
products = Product.objects.filter(is_active=True)
customers = Customer.objects.all()
return render(request, 'core/quotation_create.html', {'products': products, 'customers': customers})
@login_required
def quotation_detail(request, pk):
quotation = get_object_or_404(Quotation, pk=pk)
settings = SystemSetting.objects.first()
return render(request, 'core/quotation_detail.html', {'quotation': quotation, 'settings': settings})
@csrf_exempt
@login_required
def create_quotation_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
customer_id = data.get('customer_id')
quotation_number = data.get('quotation_number', '')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
discount = data.get('discount', 0)
valid_until = data.get('valid_until')
terms_and_conditions = data.get('terms_and_conditions', '')
notes = data.get('notes', '')
customer = None
if customer_id:
customer = Customer.objects.get(id=customer_id)
quotation = Quotation.objects.create(
customer=customer,
quotation_number=quotation_number,
total_amount=total_amount,
discount=discount,
valid_until=valid_until if valid_until else None,
terms_and_conditions=terms_and_conditions,
notes=notes,
created_by=request.user
)
for item in items:
product = Product.objects.get(id=item['id'])
QuotationItem.objects.create(
quotation=quotation,
product=product,
quantity=item['quantity'],
unit_price=item['price'],
line_total=item['line_total']
)
return JsonResponse({'success': True, 'quotation_id': quotation.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def convert_quotation_to_invoice(request, pk):
quotation = get_object_or_404(Quotation, pk=pk)
if quotation.status == 'converted':
messages.warning(request, "This quotation has already been converted to an invoice.")
return redirect('invoices')
# Create Sale from Quotation
sale = Sale.objects.create(
customer=quotation.customer,
quotation=quotation,
total_amount=quotation.total_amount,
discount=quotation.discount,
balance_due=quotation.total_amount,
payment_type='cash',
status='unpaid',
notes=quotation.notes,
created_by=request.user
)
# Create SaleItems and Update Stock
for item in quotation.items.all():
SaleItem.objects.create(
sale=sale,
product=item.product,
quantity=item.quantity,
unit_price=item.unit_price,
line_total=item.line_total
)
# Deduct Stock
item.product.stock_quantity -= item.quantity
item.product.save()
# Update Quotation Status
quotation.status = 'converted'
quotation.save()
messages.success(request, "Quotation converted to Invoice successfully!")
return redirect('invoice_detail', pk=sale.pk)
@login_required
def delete_quotation(request, pk):
quotation = get_object_or_404(Quotation, pk=pk)
quotation.delete()
messages.success(request, "Quotation deleted successfully!")
return redirect('quotations')
# --- Sale Return Views ---
@login_required
def sales_returns(request):
returns = SaleReturn.objects.all().select_related('customer', 'created_by').order_by('-created_at')
return render(request, 'core/sales_returns.html', {'returns': returns})
@login_required
def sale_return_create(request):
products = Product.objects.filter(is_active=True)
customers = Customer.objects.all()
sales = Sale.objects.all().order_by('-created_at')
return render(request, 'core/sale_return_create.html', {
'products': products,
'customers': customers,
'sales': sales
})
@login_required
def sale_return_detail(request, pk):
sale_return = get_object_or_404(SaleReturn, pk=pk)
settings = SystemSetting.objects.first()
return render(request, 'core/sale_return_detail.html', {'sale_return': sale_return, 'settings': settings})
@csrf_exempt
@login_required
def create_sale_return_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
sale_id = data.get('sale_id')
customer_id = data.get('customer_id')
return_number = data.get('return_number', '')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
notes = data.get('notes', '')
customer = None
if customer_id:
customer = Customer.objects.get(id=customer_id)
sale = None
if sale_id:
sale = Sale.objects.get(id=sale_id)
sale_return = SaleReturn.objects.create(
sale=sale,
customer=customer,
return_number=return_number,
total_amount=total_amount,
notes=notes,
created_by=request.user
)
for item in items:
product = Product.objects.get(id=item['id'])
SaleReturnItem.objects.create(
sale_return=sale_return,
product=product,
quantity=item['quantity'],
unit_price=item['price'],
line_total=item['line_total']
)
# Increase Stock for Sales Return
product.stock_quantity += int(item['quantity'])
product.save()
return JsonResponse({'success': True, 'return_id': sale_return.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def delete_sale_return(request, pk):
sale_return = get_object_or_404(SaleReturn, pk=pk)
for item in sale_return.items.all():
item.product.stock_quantity -= item.quantity
item.product.save()
sale_return.delete()
messages.success(request, "Sale return deleted successfully!")
return redirect('sales_returns')
# --- Purchase Return Views ---
@login_required
def purchase_returns(request):
returns = PurchaseReturn.objects.all().select_related('supplier', 'created_by').order_by('-created_at')
return render(request, 'core/purchase_returns.html', {'returns': returns})
@login_required
def purchase_return_create(request):
products = Product.objects.filter(is_active=True)
suppliers = Supplier.objects.all()
purchases = Purchase.objects.all().order_by('-created_at')
return render(request, 'core/purchase_return_create.html', {
'products': products,
'customers': suppliers,
'purchases': purchases
})
@login_required
def purchase_return_detail(request, pk):
purchase_return = get_object_or_404(PurchaseReturn, pk=pk)
settings = SystemSetting.objects.first()
return render(request, 'core/purchase_return_detail.html', {'purchase_return': purchase_return, 'settings': settings})
@csrf_exempt
@login_required
def create_purchase_return_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
purchase_id = data.get('purchase_id')
supplier_id = data.get('supplier_id')
return_number = data.get('return_number', '')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
notes = data.get('notes', '')
supplier = None
if supplier_id:
supplier = Supplier.objects.get(id=supplier_id)
purchase = None
if purchase_id:
purchase = Purchase.objects.get(id=purchase_id)
purchase_return = PurchaseReturn.objects.create(
purchase=purchase,
supplier=supplier,
return_number=return_number,
total_amount=total_amount,
notes=notes,
created_by=request.user
)
for item in items:
product = Product.objects.get(id=item['id'])
PurchaseReturnItem.objects.create(
purchase_return=purchase_return,
product=product,
quantity=item['quantity'],
cost_price=item['price'],
line_total=item['line_total']
)
# Decrease Stock for Purchase Return
product.stock_quantity -= int(item['quantity'])
product.save()
return JsonResponse({'success': True, 'return_id': purchase_return.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def delete_purchase_return(request, pk):
purchase_return = get_object_or_404(PurchaseReturn, pk=pk)
for item in purchase_return.items.all():
item.product.stock_quantity += item.quantity
item.product.save()
purchase_return.delete()
messages.success(request, "Purchase return deleted successfully!")
return redirect('purchase_returns')
# --- Other Management Views ---
@login_required
def reports(request):
"""
Smart Reports View
"""
# Monthly Revenue
monthly_sales = Sale.objects.annotate(month=TruncMonth('created_at')) \
.values('month') \
.annotate(total=Sum('total_amount')) \
.order_by('-month')[:12]
# Top Selling Products
top_products = SaleItem.objects.values('product__name_en', 'product__name_ar') \
.annotate(total_qty=Sum('quantity'), revenue=Sum('line_total')) \
.order_by('-total_qty')[:5]
context = {
'monthly_sales': monthly_sales,
'top_products': top_products,
}
return render(request, 'core/reports.html', context)
@login_required
def settings_view(request):
"""
Smart Admin Settings View
"""
settings = SystemSetting.objects.first()
if not settings:
settings = SystemSetting.objects.create()
if request.method == 'POST':
settings.business_name = request.POST.get('business_name')
settings.address = request.POST.get('address')
settings.phone = request.POST.get('phone')
settings.email = request.POST.get('email')
settings.currency_symbol = request.POST.get('currency_symbol')
settings.tax_rate = request.POST.get('tax_rate')
settings.decimal_places = request.POST.get('decimal_places', 3)
settings.vat_number = request.POST.get('vat_number')
settings.registration_number = request.POST.get('registration_number')
# Loyalty Settings
settings.loyalty_enabled = request.POST.get('loyalty_enabled') == 'on'
settings.points_per_currency = request.POST.get('points_per_currency', 1.0)
settings.currency_per_point = request.POST.get('currency_per_point', 0.010)
settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100)
if 'logo' in request.FILES:
settings.logo = request.FILES['logo']
settings.save()
messages.success(request, "Settings updated successfully!")
return redirect(reverse('settings') + '#profile')
payment_methods = PaymentMethod.objects.all()
loyalty_tiers = LoyaltyTier.objects.all().order_by('min_points')
return render(request, 'core/settings.html', {
'settings': settings,
'payment_methods': payment_methods,
'loyalty_tiers': loyalty_tiers
})
@login_required
def add_payment_method(request):
if request.method == 'POST':
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar')
is_active = request.POST.get('is_active') == 'on'
PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active)
messages.success(request, "Payment method added successfully!")
return redirect(reverse('settings') + '#payments')
@login_required
def edit_payment_method(request, pk):
pm = get_object_or_404(PaymentMethod, pk=pk)
if request.method == 'POST':
pm.name_en = request.POST.get('name_en')
pm.name_ar = request.POST.get('name_ar')
pm.is_active = request.POST.get('is_active') == 'on'
pm.save()
messages.success(request, "Payment method updated successfully!")
return redirect(reverse('settings') + '#payments')
@login_required
def delete_payment_method(request, pk):
pm = get_object_or_404(PaymentMethod, pk=pk)
pm.delete()
messages.success(request, "Payment method deleted successfully!")
return redirect(reverse('settings') + '#payments')
@login_required
def add_customer(request):
if request.method == 'POST':
name = request.POST.get('name')
phone = request.POST.get('phone')
email = request.POST.get('email')
address = request.POST.get('address')
Customer.objects.create(name=name, phone=phone, email=email, address=address)
messages.success(request, "Customer added successfully!")
return redirect('customers')
@login_required
def edit_customer(request, pk):
customer = get_object_or_404(Customer, pk=pk)
if request.method == 'POST':
customer.name = request.POST.get('name')
customer.phone = request.POST.get('phone')
customer.email = request.POST.get('email')
customer.address = request.POST.get('address')
customer.save()
messages.success(request, "Customer updated successfully!")
return redirect('customers')
@login_required
def delete_customer(request, pk):
customer = get_object_or_404(Customer, pk=pk)
customer.delete()
messages.success(request, "Customer deleted successfully!")
return redirect('customers')
@login_required
def add_supplier(request):
if request.method == 'POST':
name = request.POST.get('name')
contact_person = request.POST.get('contact_person')
phone = request.POST.get('phone')
Supplier.objects.create(name=name, contact_person=contact_person, phone=phone)
messages.success(request, "Supplier added successfully!")
return redirect('suppliers')
@login_required
def edit_supplier(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
if request.method == 'POST':
supplier.name = request.POST.get('name')
supplier.contact_person = request.POST.get('contact_person')
supplier.phone = request.POST.get('phone')
supplier.save()
messages.success(request, "Supplier updated successfully!")
return redirect('suppliers')
@login_required
def delete_supplier(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
supplier.delete()
messages.success(request, "Supplier deleted successfully!")
return redirect('suppliers')
@login_required
def suggest_sku(request):
"""
API endpoint to suggest a unique SKU.
"""
while True:
# Generate a random 8-digit number
sku = "".join(random.choices(string.digits, k=8))
if not Product.objects.filter(sku=sku).exists():
return JsonResponse({"sku": sku})
@login_required
def add_product(request):
if request.method == 'POST':
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar')
category_id = request.POST.get('category')
unit_id = request.POST.get('unit')
supplier_id = request.POST.get('supplier')
sku = request.POST.get('sku')
if not sku:
while True:
sku = ''.join(random.choices(string.digits, k=8))
if not Product.objects.filter(sku=sku).exists():
break
cost_price = request.POST.get('cost_price', 0)
price = request.POST.get('price', 0)
vat = request.POST.get('vat', 0)
opening_stock = request.POST.get('opening_stock', 0)
stock_quantity = request.POST.get('stock_quantity', 0)
is_active = request.POST.get('is_active') == 'on'
category = get_object_or_404(Category, id=category_id)
unit = get_object_or_404(Unit, id=unit_id) if unit_id else None
supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None
product = Product.objects.create(
name_en=name_en,
name_ar=name_ar,
category=category,
unit=unit,
supplier=supplier,
sku=sku,
cost_price=cost_price,
price=price,
vat=vat,
opening_stock=opening_stock,
stock_quantity=stock_quantity,
is_active=is_active
)
if 'image' in request.FILES:
product.image = request.FILES['image']
product.save()
messages.success(request, "Product added successfully!")
return redirect(reverse('inventory') + '#items')
@login_required
def edit_product(request, pk):
product = get_object_or_404(Product, pk=pk)
if request.method == 'POST':
product.name_en = request.POST.get('name_en')
product.name_ar = request.POST.get('name_ar')
product.sku = request.POST.get('sku')
product.category = get_object_or_404(Category, id=request.POST.get('category'))
unit_id = request.POST.get('unit')
product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None
supplier_id = request.POST.get('supplier')
product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None
product.cost_price = request.POST.get('cost_price', 0)
product.price = request.POST.get('price', 0)
product.vat = request.POST.get('vat', 0)
product.opening_stock = request.POST.get('opening_stock', 0)
product.stock_quantity = request.POST.get('stock_quantity', 0)
product.is_active = request.POST.get('is_active') == 'on'
if 'image' in request.FILES:
product.image = request.FILES['image']
product.save()
messages.success(request, "Product updated successfully!")
return redirect(reverse('inventory') + '#items')
return redirect(reverse('inventory') + '#items')
@login_required
def delete_product(request, pk):
product = get_object_or_404(Product, pk=pk)
product.delete()
messages.success(request, "Product deleted successfully!")
return redirect(reverse('inventory') + '#items')
@login_required
def add_category(request):
if request.method == 'POST':
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar')
slug = slugify(name_en)
Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug)
messages.success(request, "Category added successfully!")
return redirect(reverse('inventory') + '#categories-list')
@login_required
def edit_category(request, pk):
category = get_object_or_404(Category, pk=pk)
if request.method == 'POST':
category.name_en = request.POST.get('name_en')
category.name_ar = request.POST.get('name_ar')
category.slug = slugify(category.name_en)
category.save()
messages.success(request, "Category updated successfully!")
return redirect(reverse('inventory') + '#categories-list')
@login_required
def delete_category(request, pk):
category = get_object_or_404(Category, pk=pk)
category.delete()
messages.success(request, "Category deleted successfully!")
return redirect(reverse('inventory') + '#categories-list')
@login_required
def add_unit(request):
if request.method == 'POST':
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar')
short_name = request.POST.get('short_name')
Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name)
messages.success(request, "Unit added successfully!")
return redirect(reverse('inventory') + '#units-list')
@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 successfully!")
return redirect(reverse('inventory') + '#units-list')
@login_required
def delete_unit(request, pk):
unit = get_object_or_404(Unit, pk=pk)
unit.delete()
messages.success(request, "Unit deleted successfully!")
return redirect(reverse('inventory') + '#units-list')
@login_required
def barcode_labels(request):
products = Product.objects.filter(is_active=True).order_by('name_en')
context = {'products': products}
return render(request, 'core/barcode_labels.html', context)
@login_required
def import_products(request):
"""
Import products from an Excel (.xlsx) file.
Expected columns: Name (Eng), Name (Ar), SKU, Cost Price, Sale Price
"""
if request.method == 'POST' and request.FILES.get('excel_file'):
excel_file = request.FILES['excel_file']
if not excel_file.name.endswith('.xlsx'):
messages.error(request, "Please upload a valid .xlsx file.")
return redirect(reverse('inventory') + '#items')
try:
wb = openpyxl.load_workbook(excel_file)
sheet = wb.active
# Get or create a default category
default_category, _ = Category.objects.get_or_create(
name_en="General",
defaults={'name_ar': "عام", 'slug': 'general'}
)
count = 0
updated_count = 0
errors = []
# Skip header row (min_row=2)
for i, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2):
if not any(row): continue # Skip empty rows
# Unpack columns with fallbacks for safety
# Format: name_en, name_ar, sku, cost_price, sale_price
name_en = str(row[0]).strip() if row[0] else None
name_ar = str(row[1]).strip() if len(row) > 1 and row[1] else name_en
sku = str(row[2]).strip() if len(row) > 2 and row[2] else None
cost_price = row[3] if len(row) > 3 and row[3] is not None else 0
sale_price = row[4] if len(row) > 4 and row[4] is not None else 0
if not name_en:
errors.append(f"Row {i}: Missing English Name. Skipped.")
continue
if not sku:
# Generate unique SKU if missing
while True:
sku = "".join(random.choices(string.digits, k=8))
if not Product.objects.filter(sku=sku).exists():
break
product, created = Product.objects.update_or_create(
sku=sku,
defaults={
'name_en': name_en,
'name_ar': name_ar,
'cost_price': cost_price,
'price': sale_price,
'category': default_category,
'is_active': True
}
)
if created:
count += 1
else:
updated_count += 1
if count > 0 or updated_count > 0:
msg = f"Import completed: {count} new items added"
if updated_count > 0:
msg += f", {updated_count} items updated"
messages.success(request, msg)
if errors:
for error in errors:
messages.warning(request, error)
except Exception as e:
messages.error(request, f"Error processing file: {str(e)}")
return redirect(reverse('inventory') + '#items')
@csrf_exempt
@login_required
def add_category_ajax(request):
if request.method == 'POST':
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': 'Missing names'}, status=400)
slug = slugify(name_en)
category = Category.objects.create(name_en=name_en, name_ar=name_ar, slug=slug)
return JsonResponse({
'success': True,
'id': category.id,
'name_en': category.name_en,
'name_ar': category.name_ar
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@csrf_exempt
@login_required
def add_unit_ajax(request):
if request.method == 'POST':
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': 'Missing fields'}, status=400)
unit = Unit.objects.create(name_en=name_en, name_ar=name_ar, short_name=short_name)
return JsonResponse({
'success': True,
'id': unit.id,
'name_en': unit.name_en,
'name_ar': unit.name_ar
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@csrf_exempt
@login_required
def add_supplier_ajax(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
name = data.get('name')
contact_person = data.get('contact_person', '')
phone = data.get('phone', '')
if not name:
return JsonResponse({'success': False, 'error': 'Missing name'}, status=400)
supplier = Supplier.objects.create(name=name, contact_person=contact_person, phone=phone)
return JsonResponse({
'success': True,
'id': supplier.id,
'name': supplier.name
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def user_management(request):
if not (request.user.is_superuser or request.user.groups.filter(name='admin').exists()):
messages.error(request, "Access denied.")
return redirect('index')
users = User.objects.all().prefetch_related('groups')
groups = Group.objects.all().prefetch_related('permissions')
# Filter for relevant permissions (core and auth)
permissions = Permission.objects.select_related('content_type').all().order_by('content_type__app_label', 'codename')
if request.method == 'POST':
action = request.POST.get('action')
if action == 'add':
username = request.POST.get('username')
password = request.POST.get('password')
email = request.POST.get('email')
group_ids = request.POST.getlist('groups')
if User.objects.filter(username=username).exists():
messages.error(request, "Username already exists.")
else:
user = User.objects.create_user(username=username, email=email, password=password)
if group_ids:
selected_groups = Group.objects.filter(id__in=group_ids)
user.groups.set(selected_groups)
user.is_staff = True
user.save()
messages.success(request, f"User {username} created successfully.")
elif action == 'edit_user':
user_id = request.POST.get('user_id')
user = get_object_or_404(User, id=user_id)
user.email = request.POST.get('email')
group_ids = request.POST.getlist('groups')
selected_groups = Group.objects.filter(id__in=group_ids)
user.groups.set(selected_groups)
password = request.POST.get('password')
if password:
user.set_password(password)
user.save()
messages.success(request, f"User {user.username} updated.")
elif action == 'add_group':
name = request.POST.get('name')
permission_ids = request.POST.getlist('permissions')
if Group.objects.filter(name=name).exists():
messages.error(request, "Group name already exists.")
else:
group = Group.objects.create(name=name)
if permission_ids:
perms = Permission.objects.filter(id__in=permission_ids)
group.permissions.set(perms)
messages.success(request, f"Group {name} created successfully.")
elif action == 'edit_group':
group_id = request.POST.get('group_id')
group = get_object_or_404(Group, id=group_id)
group.name = request.POST.get('name')
permission_ids = request.POST.getlist('permissions')
perms = Permission.objects.filter(id__in=permission_ids)
group.permissions.set(perms)
group.save()
messages.success(request, f"Group {group.name} updated.")
elif action == 'delete_group':
group_id = request.POST.get('group_id')
group = get_object_or_404(Group, id=group_id)
group.delete()
messages.success(request, "Group deleted.")
elif action == 'toggle_status':
user_id = request.POST.get('user_id')
user = get_object_or_404(User, id=user_id)
if user == request.user:
messages.error(request, "You cannot deactivate yourself.")
else:
user.is_active = not user.is_active
user.save()
messages.success(request, f"User {user.username} status updated.")
# Determine redirect hash based on action
target_hash = ""
if action in ['add_group', 'edit_group', 'delete_group']:
target_hash = "#groups"
return redirect(reverse('user_management') + target_hash)
return render(request, 'core/users.html', {
'users': users,
'groups': groups,
'permissions': permissions
})
@login_required
def group_details_api(request, pk):
group = get_object_or_404(Group, pk=pk)
permissions = group.permissions.all().values_list('id', flat=True)
return JsonResponse({
'id': group.id,
'name': group.name,
'permissions': list(permissions)
})
@csrf_exempt
@login_required
def add_payment_method_ajax(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
name_en = data.get('name_en')
name_ar = data.get('name_ar')
is_active = data.get('is_active', True)
if not name_en or not name_ar:
return JsonResponse({'success': False, 'error': 'Missing names'}, status=400)
pm = PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active)
return JsonResponse({
'success': True,
'id': pm.id,
'name_en': pm.name_en,
'name_ar': pm.name_ar
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@csrf_exempt
@login_required
def add_customer_ajax(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
name = data.get('name')
phone = data.get('phone', '')
email = data.get('email', '')
address = data.get('address', '')
if not name:
return JsonResponse({'success': False, 'error': 'Missing name'}, status=400)
customer = Customer.objects.create(name=name, phone=phone, email=email, address=address)
return JsonResponse({
'success': True,
'id': customer.id,
'name': customer.name
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def hold_sale_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
customer_id = data.get('customer_id')
cart_data = data.get('items', [])
total_amount = data.get('total_amount', 0)
notes = data.get('notes', '')
customer = None
if customer_id:
customer = Customer.objects.filter(id=customer_id).first()
held_sale = HeldSale.objects.create(
customer=customer,
cart_data=cart_data,
total_amount=total_amount,
notes=notes,
created_by=request.user
)
return JsonResponse({'success': True, 'held_id': held_sale.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def get_held_sales_api(request):
held_sales = HeldSale.objects.filter(created_by=request.user).select_related('customer').order_by('-created_at')
data = []
for hs in held_sales:
data.append({
'id': hs.id,
'customer_name': hs.customer.name if hs.customer else 'Guest',
'total_amount': float(hs.total_amount),
'items_count': len(hs.cart_data),
'created_at': hs.created_at.strftime("%Y-%m-%d %H:%M"),
'notes': hs.notes
})
return JsonResponse({'success': True, 'held_sales': data})
@login_required
def recall_held_sale_api(request, pk):
held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
data = {
'success': True,
'customer_id': held_sale.customer.id if held_sale.customer else None,
'items': held_sale.cart_data,
'total_amount': float(held_sale.total_amount),
'notes': held_sale.notes
}
held_sale.delete()
return JsonResponse(data)
@login_required
def delete_held_sale_api(request, pk):
held_sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
held_sale.delete()
return JsonResponse({'success': True})
@login_required
def add_loyalty_tier(request):
if request.method == 'POST':
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar')
min_points = request.POST.get('min_points', 0)
multiplier = request.POST.get('point_multiplier', 1.0)
discount = request.POST.get('discount_percentage', 0)
color = request.POST.get('color_code', '#6c757d')
LoyaltyTier.objects.create(
name_en=name_en, name_ar=name_ar,
min_points=min_points, point_multiplier=multiplier,
discount_percentage=discount, color_code=color
)
messages.success(request, "Loyalty tier added successfully!")
return redirect(reverse('settings') + '#loyalty')
@login_required
def edit_loyalty_tier(request, pk):
tier = get_object_or_404(LoyaltyTier, pk=pk)
if request.method == 'POST':
tier.name_en = request.POST.get('name_en')
tier.name_ar = request.POST.get('name_ar')
tier.min_points = request.POST.get('min_points')
tier.point_multiplier = request.POST.get('point_multiplier')
tier.discount_percentage = request.POST.get('discount_percentage')
tier.color_code = request.POST.get('color_code')
tier.save()
messages.success(request, "Loyalty tier updated successfully!")
return redirect(reverse('settings') + '#loyalty')
@login_required
def delete_loyalty_tier(request, pk):
tier = get_object_or_404(LoyaltyTier, pk=pk)
tier.delete()
messages.success(request, "Loyalty tier deleted successfully!")
return redirect(reverse('settings') + '#loyalty')
@login_required
def get_customer_loyalty_api(request, pk):
customer = get_object_or_404(Customer, pk=pk)
settings = SystemSetting.objects.first()
tier_info = None
if customer.loyalty_tier:
tier_info = {
'name_en': customer.loyalty_tier.name_en,
'name_ar': customer.loyalty_tier.name_ar,
'multiplier': float(customer.loyalty_tier.point_multiplier),
'discount': float(customer.loyalty_tier.discount_percentage),
'color': customer.loyalty_tier.color_code
}
return JsonResponse({
'success': True,
'points': float(customer.loyalty_points),
'tier': tier_info,
'currency_per_point': float(settings.currency_per_point) if settings else 0.01,
'min_points_to_redeem': settings.min_points_to_redeem if settings else 100
})
@login_required
def profile_view(request):
"""
User Profile View
"""
if request.method == 'POST':
user = request.user
user.first_name = request.POST.get('first_name')
user.last_name = request.POST.get('last_name')
user.email = request.POST.get('email')
# Profile specific
profile = user.profile
profile.phone = request.POST.get('phone')
profile.bio = request.POST.get('bio')
if 'image' in request.FILES:
profile.image = request.FILES['image']
user.save()
profile.save()
# Password change
password = request.POST.get('password')
confirm_password = request.POST.get('confirm_password')
if password:
if password == confirm_password:
user.set_password(password)
user.save()
from django.contrib.auth import update_session_auth_hash
update_session_auth_hash(request, user)
messages.success(request, "Profile and password updated successfully!")
else:
messages.error(request, "Passwords do not match.")
else:
messages.success(request, "Profile updated successfully!")
return redirect('profile')
return render(request, 'core/profile.html')