2419 lines
92 KiB
Python
2419 lines
92 KiB
Python
import base64
|
|
import os
|
|
from django.conf import settings as django_settings
|
|
from django.utils.translation import gettext as _
|
|
from django.utils.translation import gettext_lazy as _
|
|
from .utils import number_to_words_en, send_whatsapp_document
|
|
from django.core.paginator import Paginator
|
|
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, Q
|
|
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 ( Expense, ExpenseCategory,
|
|
Product, Sale, Category, Unit, Customer, Supplier,
|
|
Purchase, PurchaseItem, PurchasePayment,
|
|
SaleItem, SalePayment, SystemSetting,
|
|
Quotation, QuotationItem,
|
|
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem,
|
|
PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction
|
|
, Device)
|
|
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()
|
|
|
|
# Expired Items Alert
|
|
today = timezone.now().date()
|
|
expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0).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, 'expired_count': expired_count,
|
|
'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_list = Product.objects.all().select_related('category', 'unit', 'supplier').order_by('-created_at')
|
|
|
|
# Filter by category
|
|
category_id = request.GET.get('category')
|
|
if category_id:
|
|
products_list = products_list.filter(category_id=category_id)
|
|
|
|
# Search
|
|
search = request.GET.get('search')
|
|
if search:
|
|
products_list = products_list.filter(
|
|
Q(name_en__icontains=search) |
|
|
Q(name_ar__icontains=search) |
|
|
Q(sku__icontains=search)
|
|
)
|
|
|
|
# Expired items
|
|
today = timezone.now().date()
|
|
expired_products = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0)
|
|
expiring_soon_products = Product.objects.filter(has_expiry=True, expiry_date__gte=today, expiry_date__lte=today + timedelta(days=30), stock_quantity__gt=0)
|
|
|
|
paginator = Paginator(products_list, 25)
|
|
page_number = request.GET.get('page')
|
|
products = paginator.get_page(page_number)
|
|
|
|
categories = Category.objects.all()
|
|
suppliers = Supplier.objects.all()
|
|
units = Unit.objects.all()
|
|
|
|
context = {
|
|
'products': products,
|
|
'categories': categories,
|
|
'suppliers': suppliers,
|
|
'units': units,
|
|
'expired_products': expired_products,
|
|
'expiring_soon_products': expiring_soon_products,
|
|
'today': today
|
|
}
|
|
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_qs = Customer.objects.all().annotate(total_sales=Sum('sales__total_amount')).order_by('name')
|
|
paginator = Paginator(customers_qs, 25)
|
|
page_number = request.GET.get('page')
|
|
customers_list = paginator.get_page(page_number)
|
|
context = {'customers': customers_list}
|
|
return render(request, 'core/customers.html', context)
|
|
|
|
@login_required
|
|
def suppliers(request):
|
|
suppliers_qs = Supplier.objects.all().order_by('name')
|
|
paginator = Paginator(suppliers_qs, 25)
|
|
page_number = request.GET.get('page')
|
|
suppliers_list = paginator.get_page(page_number)
|
|
context = {'suppliers': suppliers_list}
|
|
return render(request, 'core/suppliers.html', context)
|
|
|
|
# --- Purchase Views ---
|
|
|
|
@login_required
|
|
def supplier_payments(request):
|
|
payments_qs = PurchasePayment.objects.all().select_related("purchase", "purchase__supplier", "payment_method", "created_by").order_by("-payment_date", "-id")
|
|
paginator = Paginator(payments_qs, 25)
|
|
page_number = request.GET.get("page")
|
|
payments = paginator.get_page(page_number)
|
|
return render(request, "core/supplier_payments.html", {"payments": payments})
|
|
|
|
def purchases(request):
|
|
purchases_qs = Purchase.objects.all().select_related('supplier', 'created_by').order_by('-created_at')
|
|
paginator = Paginator(purchases_qs, 25)
|
|
page_number = request.GET.get('page')
|
|
purchases_list = paginator.get_page(page_number)
|
|
suppliers_qs = Supplier.objects.all().order_by('name')
|
|
paginator = Paginator(suppliers_qs, 25)
|
|
page_number = request.GET.get('page')
|
|
suppliers_list = paginator.get_page(page_number)
|
|
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,
|
|
'amount_in_words': number_to_words_en(purchase.total_amount)
|
|
})
|
|
|
|
@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'])
|
|
item_expiry = item.get('expiry_date')
|
|
PurchaseItem.objects.create(
|
|
purchase=purchase,
|
|
product=product,
|
|
quantity=item['quantity'],
|
|
cost_price=item['price'],
|
|
expiry_date=item_expiry if item_expiry else None,
|
|
line_total=item['line_total']
|
|
)
|
|
# Update Stock
|
|
product.stock_quantity += int(item['quantity'])
|
|
product.cost_price = item['price']
|
|
|
|
if item_expiry:
|
|
product.has_expiry = True
|
|
if not product.expiry_date or str(item_expiry) > str(product.expiry_date):
|
|
product.expiry_date = item_expiry
|
|
|
|
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")
|
|
|
|
# Filtering
|
|
start_date = request.GET.get("start_date")
|
|
end_date = request.GET.get("end_date")
|
|
customer_id = request.GET.get("customer")
|
|
status = request.GET.get("status")
|
|
|
|
if start_date:
|
|
sales = sales.filter(created_at__date__gte=start_date)
|
|
if end_date:
|
|
sales = sales.filter(created_at__date__lte=end_date)
|
|
if customer_id:
|
|
sales = sales.filter(customer_id=customer_id)
|
|
if status:
|
|
sales = sales.filter(status=status)
|
|
|
|
sales = sales.order_by("-created_at")
|
|
paginator = Paginator(sales, 25)
|
|
page_number = request.GET.get("page")
|
|
sales = paginator.get_page(page_number)
|
|
|
|
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,
|
|
'amount_in_words': number_to_words_en(sale.total_amount)
|
|
})
|
|
|
|
@login_required
|
|
def edit_invoice(request, pk):
|
|
sale = get_object_or_404(Sale, pk=pk)
|
|
# Prepare cart items for JSON
|
|
cart_items = []
|
|
for item in sale.items.all():
|
|
cart_items.append({
|
|
'id': item.product.id,
|
|
'name_en': item.product.name_en,
|
|
'sku': item.product.sku,
|
|
'price': float(item.unit_price),
|
|
'quantity': item.quantity
|
|
})
|
|
|
|
customers = Customer.objects.all()
|
|
products = Product.objects.filter(is_active=True)
|
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
|
|
|
# Find initial payment method
|
|
initial_payment = sale.payments.filter(notes='Initial payment').first()
|
|
payment_method_id = initial_payment.payment_method_id if initial_payment else ''
|
|
|
|
return render(request, 'core/invoice_edit.html', {
|
|
'sale': sale,
|
|
'customers': customers,
|
|
'products': products,
|
|
'payment_methods': payment_methods,
|
|
'cart_json': json.dumps(cart_items),
|
|
'payment_method_id': payment_method_id
|
|
})
|
|
|
|
@csrf_exempt
|
|
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)
|
|
|
|
if not customer and payment_type != 'cash':
|
|
return JsonResponse({'success': False, 'error': _('Credit or Partial payments are not allowed for Guest customers.')}, status=400)
|
|
|
|
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)
|
|
|
|
@csrf_exempt
|
|
@login_required
|
|
def send_invoice_whatsapp(request):
|
|
if request.method == 'POST':
|
|
try:
|
|
data = json.loads(request.body)
|
|
sale_id = data.get('sale_id')
|
|
phone = data.get('phone')
|
|
pdf_base64 = data.get('pdf_data')
|
|
|
|
if not phone or not pdf_base64:
|
|
return JsonResponse({'success': False, 'error': 'Missing phone or PDF data.'}, status=400)
|
|
|
|
if ',' in pdf_base64:
|
|
pdf_base64 = pdf_base64.split(',')[1]
|
|
|
|
pdf_content = base64.b64decode(pdf_base64)
|
|
|
|
temp_dir = os.path.join(django_settings.MEDIA_ROOT, 'temp_invoices')
|
|
if not os.path.exists(temp_dir):
|
|
os.makedirs(temp_dir)
|
|
|
|
filename = f'Invoice_{sale_id}.pdf'
|
|
file_path_pdf = os.path.join(temp_dir, filename)
|
|
|
|
with open(file_path_pdf, 'wb') as f_pdf:
|
|
f_pdf.write(pdf_content)
|
|
|
|
base_url = request.build_absolute_uri('/')
|
|
document_url = f"{base_url.rstrip('/')}{django_settings.MEDIA_URL}temp_invoices/{filename}"
|
|
|
|
sale = Sale.objects.filter(id=sale_id).first()
|
|
invoice_num = sale.invoice_number if sale and sale.invoice_number else sale_id
|
|
caption = f'Invoice #{invoice_num}'
|
|
|
|
success, message = send_whatsapp_document(phone, document_url, caption)
|
|
|
|
return JsonResponse({'success': success, 'message': message})
|
|
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
|
|
|
return JsonResponse({'success': False, 'error': 'Invalid request method.'}, 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_qs = Quotation.objects.all().select_related('customer', 'created_by').order_by('-created_at')
|
|
paginator = Paginator(quotations_qs, 25)
|
|
page_number = request.GET.get('page')
|
|
quotations_list = paginator.get_page(page_number)
|
|
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,
|
|
'amount_in_words': number_to_words_en(quotation.total_amount)
|
|
})
|
|
|
|
@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_qs = SaleReturn.objects.all().select_related('customer', 'created_by').order_by('-created_at')
|
|
paginator = Paginator(returns_qs, 25)
|
|
page_number = request.GET.get('page')
|
|
returns = paginator.get_page(page_number)
|
|
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,
|
|
'amount_in_words': number_to_words_en(sale_return.total_amount)
|
|
})
|
|
|
|
@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_qs = PurchaseReturn.objects.all().select_related('supplier', 'created_by').order_by('-created_at')
|
|
paginator = Paginator(returns_qs, 25)
|
|
page_number = request.GET.get('page')
|
|
returns = paginator.get_page(page_number)
|
|
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,
|
|
'amount_in_words': number_to_words_en(purchase_return.total_amount)
|
|
})
|
|
|
|
@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":
|
|
if "business_name" in request.POST:
|
|
settings.business_name = request.POST.get("business_name") or "Meezan Accounting"
|
|
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", "OMR")
|
|
settings.tax_rate = request.POST.get("tax_rate", 0)
|
|
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", "")
|
|
|
|
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"]
|
|
|
|
elif "wablas_token" in request.POST or "wablas_enabled" in request.POST or "wablas_server_url" in request.POST:
|
|
settings.wablas_enabled = request.POST.get("wablas_enabled") == "on"
|
|
settings.wablas_token = request.POST.get("wablas_token", "")
|
|
settings.wablas_server_url = request.POST.get("wablas_server_url", "")
|
|
settings.wablas_secret_key = request.POST.get("wablas_secret_key", "")
|
|
|
|
settings.save()
|
|
messages.success(request, _("Settings updated successfully!"))
|
|
|
|
if "business_name" in request.POST:
|
|
return redirect(reverse("settings") + "#profile")
|
|
elif "wablas_token" in request.POST or "wablas_enabled" in request.POST or "wablas_server_url" in request.POST:
|
|
return redirect(reverse("settings") + "#whatsapp")
|
|
else:
|
|
return redirect(reverse("settings"))
|
|
|
|
payment_methods = PaymentMethod.objects.all().order_by("name_en")
|
|
loyalty_tiers = LoyaltyTier.objects.all().order_by("min_points")
|
|
devices = Device.objects.all().order_by("name")
|
|
|
|
context = {
|
|
"settings": settings,
|
|
"payment_methods": payment_methods,
|
|
"loyalty_tiers": loyalty_tiers,
|
|
"devices": devices
|
|
}
|
|
return render(request, "core/settings.html", context)
|
|
|
|
@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'
|
|
has_expiry = request.POST.get('has_expiry') == 'on'
|
|
expiry_date = request.POST.get('expiry_date')
|
|
if not has_expiry:
|
|
expiry_date = None
|
|
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)
|
|
description = request.POST.get('description', '')
|
|
opening_stock = request.POST.get('opening_stock', 0)
|
|
stock_quantity = request.POST.get('stock_quantity', 0)
|
|
is_active = request.POST.get('is_active') == 'on'
|
|
has_expiry = request.POST.get('has_expiry') == 'on'
|
|
expiry_date = request.POST.get('expiry_date')
|
|
if not has_expiry:
|
|
expiry_date = None
|
|
|
|
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,
|
|
description=description,
|
|
opening_stock=opening_stock,
|
|
stock_quantity=stock_quantity,
|
|
is_active=is_active,
|
|
has_expiry=has_expiry,
|
|
min_stock_level=request.POST.get('min_stock_level', 0),
|
|
expiry_date=expiry_date
|
|
)
|
|
|
|
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.description = request.POST.get('description', '')
|
|
product.opening_stock = request.POST.get('opening_stock', 0)
|
|
product.stock_quantity = request.POST.get('stock_quantity', 0)
|
|
product.min_stock_level = request.POST.get('min_stock_level', 0)
|
|
product.is_active = request.POST.get('is_active') == 'on'
|
|
product.has_expiry = request.POST.get('has_expiry') == 'on'
|
|
product.expiry_date = request.POST.get('expiry_date')
|
|
if not product.has_expiry:
|
|
product.expiry_date = None
|
|
|
|
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')
|
|
|
|
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_qs = User.objects.all().prefetch_related('groups').order_by('username')
|
|
paginator = Paginator(users_qs, 25)
|
|
page_number = request.GET.get('page')
|
|
users = paginator.get_page(page_number)
|
|
groups = Group.objects.all().prefetch_related('permissions')
|
|
# Filter for relevant permissions (core and auth)
|
|
excluded_apps = ['admin', 'auth', 'contenttypes', 'sessions']
|
|
permissions = Permission.objects.select_related('content_type').exclude(content_type__app_label__in=excluded_apps).order_by('content_type__app_label', 'content_type__model', '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,
|
|
'customer_name': held_sale.customer.name if held_sale.customer else "",
|
|
'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')
|
|
|
|
# --- Expenses Views ---
|
|
|
|
@login_required
|
|
def expenses_view(request):
|
|
"""
|
|
List and filter expenses
|
|
"""
|
|
expenses = Expense.objects.all().order_by('-date', '-created_at')
|
|
|
|
# Filtering
|
|
start_date = request.GET.get('start_date')
|
|
end_date = request.GET.get('end_date')
|
|
category_id = request.GET.get('category')
|
|
|
|
if start_date:
|
|
expenses = expenses.filter(date__gte=start_date)
|
|
if end_date:
|
|
expenses = expenses.filter(date__lte=end_date)
|
|
if category_id:
|
|
expenses = expenses.filter(category_id=category_id)
|
|
|
|
paginator = Paginator(expenses, 25)
|
|
page_number = request.GET.get('page')
|
|
expenses = paginator.get_page(page_number)
|
|
categories = ExpenseCategory.objects.all()
|
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
|
|
|
context = {
|
|
'expenses': expenses,
|
|
'categories': categories,
|
|
'payment_methods': payment_methods,
|
|
'start_date': start_date,
|
|
'end_date': end_date,
|
|
'category_id': category_id,
|
|
}
|
|
return render(request, 'core/expenses.html', context)
|
|
|
|
@login_required
|
|
def expense_create_view(request):
|
|
"""
|
|
Create a new expense
|
|
"""
|
|
if request.method == 'POST':
|
|
category_id = request.POST.get('category')
|
|
amount = request.POST.get('amount')
|
|
date = request.POST.get('date') or timezone.now().date()
|
|
description = request.POST.get('description', '')
|
|
payment_method_id = request.POST.get('payment_method')
|
|
attachment = request.FILES.get('attachment')
|
|
|
|
category = get_object_or_404(ExpenseCategory, id=category_id)
|
|
pm = None
|
|
if payment_method_id:
|
|
pm = get_object_or_404(PaymentMethod, id=payment_method_id)
|
|
|
|
Expense.objects.create(
|
|
category=category,
|
|
amount=amount,
|
|
date=date,
|
|
description=description,
|
|
payment_method=pm,
|
|
attachment=attachment,
|
|
created_by=request.user
|
|
)
|
|
messages.success(request, _("Expense recorded successfully!"))
|
|
|
|
return redirect('expenses')
|
|
|
|
@login_required
|
|
def expense_delete_view(request, pk):
|
|
"""
|
|
Delete an expense
|
|
"""
|
|
expense = get_object_or_404(Expense, pk=pk)
|
|
expense.delete()
|
|
messages.success(request, _("Expense deleted successfully!"))
|
|
return redirect('expenses')
|
|
|
|
@login_required
|
|
def expense_categories_view(request):
|
|
"""
|
|
Manage expense categories
|
|
"""
|
|
if request.method == 'POST':
|
|
category_id = request.POST.get('category_id')
|
|
name_en = request.POST.get('name_en')
|
|
name_ar = request.POST.get('name_ar')
|
|
description = request.POST.get('description', '')
|
|
|
|
if category_id:
|
|
category = get_object_or_404(ExpenseCategory, id=category_id)
|
|
category.name_en = name_en
|
|
category.name_ar = name_ar
|
|
category.description = description
|
|
category.save()
|
|
messages.success(request, _("Expense category updated successfully!"))
|
|
else:
|
|
ExpenseCategory.objects.create(
|
|
name_en=name_en,
|
|
name_ar=name_ar,
|
|
description=description
|
|
)
|
|
messages.success(request, _("Expense category created successfully!"))
|
|
return redirect('expense_categories')
|
|
|
|
categories = ExpenseCategory.objects.all().order_by('name_en')
|
|
return render(request, 'core/expense_categories.html', {'categories': categories})
|
|
|
|
@login_required
|
|
def expense_category_delete_view(request, pk):
|
|
"""
|
|
Delete an expense category
|
|
"""
|
|
category = get_object_or_404(ExpenseCategory, pk=pk)
|
|
category.delete()
|
|
messages.success(request, _("Expense category deleted successfully!"))
|
|
return redirect('expense_categories')
|
|
@csrf_exempt
|
|
@login_required
|
|
def update_sale_api(request, pk):
|
|
if request.method == 'POST':
|
|
try:
|
|
sale = get_object_or_404(Sale, pk=pk)
|
|
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', '')
|
|
points_to_redeem = data.get('loyalty_points_redeemed', 0)
|
|
|
|
settings = SystemSetting.objects.first()
|
|
if not settings:
|
|
settings = SystemSetting.objects.create()
|
|
|
|
# 1. Restore Stock
|
|
for item in sale.items.all():
|
|
item.product.stock_quantity += item.quantity
|
|
item.product.save()
|
|
|
|
# 2. Reverse Loyalty Points for the old customer
|
|
if sale.customer and settings.loyalty_enabled:
|
|
for lt in sale.loyalty_transactions.all():
|
|
sale.customer.loyalty_points -= decimal.Decimal(str(lt.points))
|
|
lt.delete()
|
|
sale.customer.update_tier()
|
|
sale.customer.save()
|
|
|
|
# 3. Update Sale Metadata
|
|
customer = None
|
|
if customer_id:
|
|
customer = Customer.objects.get(id=customer_id)
|
|
|
|
if not customer and payment_type != 'cash':
|
|
return JsonResponse({'success': False, 'error': _('Credit or Partial payments are not allowed for Guest customers.')}, status=400)
|
|
|
|
sale.customer = customer
|
|
sale.invoice_number = invoice_number
|
|
sale.total_amount = total_amount
|
|
sale.discount = discount
|
|
sale.payment_type = payment_type
|
|
sale.due_date = due_date if due_date else None
|
|
sale.notes = notes
|
|
|
|
# Loyalty discount recalculation
|
|
loyalty_discount = 0
|
|
if settings.loyalty_enabled and customer and points_to_redeem > 0:
|
|
if float(customer.loyalty_points) >= float(points_to_redeem):
|
|
loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point)
|
|
|
|
sale.loyalty_points_redeemed = points_to_redeem
|
|
sale.loyalty_discount_amount = loyalty_discount
|
|
sale.save()
|
|
|
|
# 4. Handle Items (Delete old, Create new)
|
|
sale.items.all().delete()
|
|
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()
|
|
|
|
# 5. Handle Payments
|
|
sale.paid_amount = paid_amount
|
|
sale.balance_due = float(total_amount) - float(paid_amount)
|
|
|
|
if float(paid_amount) >= float(total_amount):
|
|
sale.status = 'paid'
|
|
elif float(paid_amount) > 0:
|
|
sale.status = 'partial'
|
|
else:
|
|
sale.status = 'unpaid'
|
|
sale.save()
|
|
|
|
pm = None
|
|
if payment_method_id:
|
|
pm = PaymentMethod.objects.filter(id=payment_method_id).first()
|
|
|
|
initial_payment = sale.payments.filter(notes="Initial payment").first()
|
|
if initial_payment:
|
|
initial_payment.amount = paid_amount
|
|
initial_payment.payment_method = pm
|
|
initial_payment.payment_method_name = pm.name_en if pm else payment_type.capitalize()
|
|
initial_payment.save()
|
|
elif float(paid_amount) > 0:
|
|
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
|
|
)
|
|
|
|
# 6. Re-apply Loyalty for the (possibly new) customer
|
|
if settings.loyalty_enabled and customer:
|
|
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 Updated Sale #{sale.id}"
|
|
)
|
|
|
|
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 Updated Sale #{sale.id}"
|
|
)
|
|
|
|
customer.update_tier()
|
|
customer.save()
|
|
|
|
return JsonResponse({'success': True})
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
|
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
|
|
|
|
@login_required
|
|
def customer_payments(request):
|
|
"""
|
|
List of payments received from customers
|
|
"""
|
|
payments_qs = SalePayment.objects.all().select_related("sale", "sale__customer", "payment_method", "created_by").order_by("-payment_date", "-id")
|
|
|
|
# Filtering
|
|
start_date = request.GET.get("start_date")
|
|
end_date = request.GET.get("end_date")
|
|
customer_id = request.GET.get("customer")
|
|
|
|
if start_date:
|
|
payments_qs = payments_qs.filter(payment_date__gte=start_date)
|
|
if end_date:
|
|
payments_qs = payments_qs.filter(payment_date__lte=end_date)
|
|
if customer_id:
|
|
payments_qs = payments_qs.filter(sale__customer_id=customer_id)
|
|
|
|
paginator = Paginator(payments_qs, 25)
|
|
page_number = request.GET.get("page")
|
|
payments = paginator.get_page(page_number)
|
|
|
|
customers = Customer.objects.all().order_by("name")
|
|
|
|
return render(request, "core/customer_payments.html", {
|
|
"payments": payments,
|
|
"customers": customers,
|
|
"start_date": start_date,
|
|
"end_date": end_date,
|
|
"customer_id": customer_id,
|
|
})
|
|
|
|
@login_required
|
|
@login_required
|
|
def sale_receipt(request, pk):
|
|
"""
|
|
Printable receipt for a fully paid sale
|
|
"""
|
|
sale = get_object_or_404(Sale, pk=pk)
|
|
settings = SystemSetting.objects.first()
|
|
return render(request, "core/sale_receipt.html", {
|
|
"sale": sale,
|
|
"settings": settings,
|
|
"amount_in_words": number_to_words_en(sale.total_amount)
|
|
})
|
|
|
|
def customer_payment_receipt(request, pk):
|
|
"""
|
|
Printable receipt for a customer payment
|
|
"""
|
|
payment = get_object_or_404(SalePayment, pk=pk)
|
|
settings = SystemSetting.objects.first()
|
|
return render(request, "core/customer_payment_receipt.html", {
|
|
"payment": payment,
|
|
"settings": settings,
|
|
"amount_in_words": number_to_words_en(payment.amount)
|
|
})
|
|
|
|
@login_required
|
|
def customer_statement(request):
|
|
"""
|
|
Generate a transaction statement for a specific customer.
|
|
"""
|
|
customers = Customer.objects.all().order_by('name')
|
|
customer_id = request.GET.get('customer')
|
|
start_date = request.GET.get('start_date')
|
|
end_date = request.GET.get('end_date')
|
|
|
|
statement_data = []
|
|
customer = None
|
|
opening_balance = 0
|
|
|
|
if customer_id:
|
|
customer = get_object_or_404(Customer, id=customer_id)
|
|
|
|
# Calculate opening balance before start_date
|
|
if start_date:
|
|
sales_before = Sale.objects.filter(customer=customer, created_at__date__lt=start_date).aggregate(total=Sum('total_amount'))['total'] or 0
|
|
returns_before = SaleReturn.objects.filter(customer=customer, created_at__date__lt=start_date).aggregate(total=Sum('total_amount'))['total'] or 0
|
|
payments_before = SalePayment.objects.filter(sale__customer=customer, payment_date__lt=start_date).aggregate(total=Sum('amount'))['total'] or 0
|
|
opening_balance = float(sales_before) - float(returns_before) - float(payments_before)
|
|
|
|
# Fetch transactions within range
|
|
sales = Sale.objects.filter(customer=customer)
|
|
returns = SaleReturn.objects.filter(customer=customer)
|
|
payments = SalePayment.objects.filter(sale__customer=customer)
|
|
|
|
if start_date:
|
|
sales = sales.filter(created_at__date__gte=start_date)
|
|
returns = returns.filter(created_at__date__gte=start_date)
|
|
payments = payments.filter(payment_date__gte=start_date)
|
|
if end_date:
|
|
sales = sales.filter(created_at__date__lte=end_date)
|
|
returns = returns.filter(created_at__date__lte=end_date)
|
|
payments = payments.filter(payment_date__lte=end_date)
|
|
|
|
for sale in sales:
|
|
statement_data.append({
|
|
'date': sale.created_at.date(),
|
|
'type': _('Sale Invoice'),
|
|
'reference': sale.invoice_number or f"#{sale.id}",
|
|
'debit': float(sale.total_amount),
|
|
'credit': 0,
|
|
})
|
|
for ret in returns:
|
|
statement_data.append({
|
|
'date': ret.created_at.date(),
|
|
'type': _('Sale Return'),
|
|
'reference': ret.return_number or f"#{ret.id}",
|
|
'debit': 0,
|
|
'credit': float(ret.total_amount),
|
|
})
|
|
for pay in payments:
|
|
statement_data.append({
|
|
'date': pay.payment_date,
|
|
'type': _('Payment'),
|
|
'reference': pay.notes or _('Payment Received'),
|
|
'debit': 0,
|
|
'credit': float(pay.amount),
|
|
})
|
|
|
|
statement_data.sort(key=lambda x: (x['date'], x['type']))
|
|
|
|
running_balance = opening_balance
|
|
for item in statement_data:
|
|
running_balance += item['debit'] - item['credit']
|
|
item['balance'] = running_balance
|
|
|
|
settings = SystemSetting.objects.first()
|
|
context = {
|
|
'customers': customers,
|
|
'customer': customer,
|
|
'statement_data': statement_data,
|
|
'opening_balance': opening_balance, 'opening_balance_abs': abs(opening_balance),
|
|
'start_date': start_date,
|
|
'end_date': end_date,
|
|
'settings': settings
|
|
}
|
|
return render(request, 'core/customer_statement.html', context)
|
|
|
|
@login_required
|
|
def supplier_statement(request):
|
|
"""
|
|
Generate a transaction statement for a specific supplier.
|
|
"""
|
|
suppliers = Supplier.objects.all().order_by('name')
|
|
supplier_id = request.GET.get('supplier')
|
|
start_date = request.GET.get('start_date')
|
|
end_date = request.GET.get('end_date')
|
|
|
|
statement_data = []
|
|
supplier = None
|
|
opening_balance = 0
|
|
|
|
if supplier_id:
|
|
supplier = get_object_or_404(Supplier, id=supplier_id)
|
|
|
|
# Calculate opening balance before start_date
|
|
if start_date:
|
|
purchases_before = Purchase.objects.filter(supplier=supplier, created_at__date__lt=start_date).aggregate(total=Sum('total_amount'))['total'] or 0
|
|
returns_before = PurchaseReturn.objects.filter(supplier=supplier, created_at__date__lt=start_date).aggregate(total=Sum('total_amount'))['total'] or 0
|
|
payments_before = PurchasePayment.objects.filter(purchase__supplier=supplier, payment_date__lt=start_date).aggregate(total=Sum('amount'))['total'] or 0
|
|
opening_balance = float(purchases_before) - float(returns_before) - float(payments_before)
|
|
|
|
# Fetch transactions within range
|
|
purchases = Purchase.objects.filter(supplier=supplier)
|
|
returns = PurchaseReturn.objects.filter(supplier=supplier)
|
|
payments = PurchasePayment.objects.filter(purchase__supplier=supplier)
|
|
|
|
if start_date:
|
|
purchases = purchases.filter(created_at__date__gte=start_date)
|
|
returns = returns.filter(created_at__date__gte=start_date)
|
|
payments = payments.filter(payment_date__gte=start_date)
|
|
if end_date:
|
|
purchases = purchases.filter(created_at__date__lte=end_date)
|
|
returns = returns.filter(created_at__date__lte=end_date)
|
|
payments = payments.filter(payment_date__lte=end_date)
|
|
|
|
for purchase in purchases:
|
|
statement_data.append({
|
|
'date': purchase.created_at.date(),
|
|
'type': _('Purchase Invoice'),
|
|
'reference': purchase.invoice_number or f"#{purchase.id}",
|
|
'debit': float(purchase.total_amount),
|
|
'credit': 0,
|
|
})
|
|
for ret in returns:
|
|
statement_data.append({
|
|
'date': ret.created_at.date(),
|
|
'type': _('Purchase Return'),
|
|
'reference': ret.return_number or f"#{ret.id}",
|
|
'debit': 0,
|
|
'credit': float(ret.total_amount),
|
|
})
|
|
for pay in payments:
|
|
statement_data.append({
|
|
'date': pay.payment_date,
|
|
'type': _('Payment'),
|
|
'reference': pay.notes or _('Payment Sent'),
|
|
'debit': 0,
|
|
'credit': float(pay.amount),
|
|
})
|
|
|
|
statement_data.sort(key=lambda x: (x['date'], x['type']))
|
|
|
|
running_balance = opening_balance
|
|
for item in statement_data:
|
|
running_balance += item['debit'] - item['credit']
|
|
item['balance'] = running_balance
|
|
|
|
settings = SystemSetting.objects.first()
|
|
context = {
|
|
'suppliers': suppliers,
|
|
'supplier': supplier,
|
|
'statement_data': statement_data,
|
|
'opening_balance': opening_balance, 'opening_balance_abs': abs(opening_balance),
|
|
'start_date': start_date,
|
|
'end_date': end_date,
|
|
'settings': settings
|
|
}
|
|
return render(request, 'core/supplier_statement.html', context)
|
|
|
|
@login_required
|
|
def cashflow_report(request):
|
|
"""
|
|
Generate a Cashflow report summarizing income and expenses.
|
|
"""
|
|
start_date = request.GET.get('start_date')
|
|
end_date = request.GET.get('end_date')
|
|
|
|
# Defaults to current month if no dates provided
|
|
if not start_date:
|
|
start_date = timezone.now().date().replace(day=1).strftime('%Y-%m-%d')
|
|
if not end_date:
|
|
end_date = timezone.now().date().strftime('%Y-%m-%d')
|
|
|
|
# Fetching Inflows (Sale Payments)
|
|
sale_payments = SalePayment.objects.all().select_related('sale', 'sale__customer')
|
|
if start_date:
|
|
sale_payments = sale_payments.filter(payment_date__gte=start_date)
|
|
if end_date:
|
|
sale_payments = sale_payments.filter(payment_date__lte=end_date)
|
|
|
|
# Fetching Outflows (Purchase Payments)
|
|
purchase_payments = PurchasePayment.objects.all().select_related('purchase', 'purchase__supplier')
|
|
if start_date:
|
|
purchase_payments = purchase_payments.filter(payment_date__gte=start_date)
|
|
if end_date:
|
|
purchase_payments = purchase_payments.filter(payment_date__lte=end_date)
|
|
|
|
# Fetching Outflows (Expenses)
|
|
expenses = Expense.objects.all().select_related('category', 'payment_method')
|
|
if start_date:
|
|
expenses = expenses.filter(date__gte=start_date)
|
|
if end_date:
|
|
expenses = expenses.filter(date__lte=end_date)
|
|
|
|
# Prepare detailed transactions list
|
|
transactions = []
|
|
|
|
for pay in sale_payments:
|
|
transactions.append({
|
|
'date': pay.payment_date,
|
|
'type': _('Sale Payment'),
|
|
'reference': pay.sale.invoice_number or f"Sale #{pay.sale.id}",
|
|
'contact': pay.sale.customer.name if pay.sale.customer else _('Guest'),
|
|
'inflow': float(pay.amount),
|
|
'outflow': 0,
|
|
'method': pay.payment_method_name
|
|
})
|
|
|
|
for pay in purchase_payments:
|
|
transactions.append({
|
|
'date': pay.payment_date,
|
|
'type': _('Purchase Payment'),
|
|
'reference': pay.purchase.invoice_number or f"Purchase #{pay.purchase.id}",
|
|
'contact': pay.purchase.supplier.name if pay.purchase.supplier else 'N/A',
|
|
'inflow': 0,
|
|
'outflow': float(pay.amount),
|
|
'method': pay.payment_method_name
|
|
})
|
|
|
|
for exp in expenses:
|
|
transactions.append({
|
|
'date': exp.date,
|
|
'type': _('Expense'),
|
|
'reference': exp.category.name_en,
|
|
'contact': _('Various'),
|
|
'inflow': 0,
|
|
'outflow': float(exp.amount),
|
|
'method': exp.payment_method.name_en if exp.payment_method else _('N/A')
|
|
})
|
|
|
|
transactions.sort(key=lambda x: x['date'], reverse=True)
|
|
|
|
total_inflow = sum(item['inflow'] for item in transactions)
|
|
total_outflow = sum(item['outflow'] for item in transactions)
|
|
net_cashflow = total_inflow - total_outflow
|
|
|
|
settings = SystemSetting.objects.first()
|
|
|
|
context = {
|
|
'transactions': transactions,
|
|
'total_inflow': total_inflow,
|
|
'total_outflow': total_outflow,
|
|
'net_cashflow': net_cashflow,
|
|
'start_date': start_date,
|
|
'end_date': end_date,
|
|
'settings': settings
|
|
}
|
|
return render(request, 'core/cashflow_report.html', context)
|
|
|
|
|
|
@login_required
|
|
def test_whatsapp_connection(request):
|
|
"""
|
|
AJAX view to test the WhatsApp connection.
|
|
"""
|
|
if request.method == 'POST':
|
|
try:
|
|
data = json.loads(request.body)
|
|
phone = data.get('phone')
|
|
if not phone:
|
|
return JsonResponse({'success': False, 'error': _("Phone number is required.")})
|
|
|
|
from .utils import send_whatsapp_message
|
|
success, message = send_whatsapp_message(phone, _("Hello! This is a test message from your Meezan Smart Admin WhatsApp Gateway."))
|
|
|
|
return JsonResponse({'success': success, 'message': message})
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)})
|
|
return JsonResponse({'success': False, 'error': _("Invalid request method.")})
|
|
|
|
|
|
@login_required
|
|
def add_device(request):
|
|
if request.method == 'POST':
|
|
name = request.POST.get('name')
|
|
device_type = request.POST.get('device_type')
|
|
connection_type = request.POST.get('connection_type')
|
|
ip_address = request.POST.get('ip_address')
|
|
port = request.POST.get('port')
|
|
is_active = request.POST.get('is_active') == 'on'
|
|
|
|
Device.objects.create(
|
|
name=name,
|
|
device_type=device_type,
|
|
connection_type=connection_type,
|
|
ip_address=ip_address if ip_address else None,
|
|
port=port if port else None,
|
|
is_active=is_active
|
|
)
|
|
messages.success(request, _("Device added successfully!"))
|
|
return redirect(reverse('settings') + '#devices')
|
|
|
|
@login_required
|
|
def edit_device(request, pk):
|
|
device = get_object_or_404(Device, pk=pk)
|
|
if request.method == 'POST':
|
|
device.name = request.POST.get('name')
|
|
device.device_type = request.POST.get('device_type')
|
|
device.connection_type = request.POST.get('connection_type')
|
|
device.ip_address = request.POST.get('ip_address')
|
|
device.port = request.POST.get('port')
|
|
device.is_active = request.POST.get('is_active') == 'on'
|
|
|
|
if not device.ip_address:
|
|
device.ip_address = None
|
|
if not device.port:
|
|
device.port = None
|
|
|
|
device.save()
|
|
messages.success(request, _("Device updated successfully!"))
|
|
return redirect(reverse('settings') + '#devices')
|
|
|
|
@login_required
|
|
def delete_device(request, pk):
|
|
device = get_object_or_404(Device, pk=pk)
|
|
device.delete()
|
|
messages.success(request, _("Device deleted successfully!"))
|
|
return redirect(reverse('settings') + '#devices')
|
|
@login_required
|
|
def search_customers_api(request):
|
|
query = request.GET.get('q', '')
|
|
if query:
|
|
customers = Customer.objects.filter(
|
|
Q(name__icontains=query) | Q(phone__icontains=query)
|
|
).values('id', 'name', 'phone')[:20]
|
|
else:
|
|
customers = []
|
|
return JsonResponse({'results': list(customers)}) |