1204 lines
45 KiB
Python
1204 lines
45 KiB
Python
from django.db import models, transaction
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils import timezone
|
|
from django.contrib.auth.models import User
|
|
from django.db.models.signals import post_save
|
|
from django.dispatch import receiver
|
|
import base64
|
|
import os
|
|
from django.conf import settings as django_settings
|
|
from django.utils.translation import gettext as _, get_language
|
|
from django.utils.formats import date_format
|
|
# Changed to use helpers to avoid circular imports and requests issue
|
|
from .helpers 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, HttpResponse
|
|
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, PurchaseOrder, PurchaseOrderItem,
|
|
PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction,
|
|
Device, CashierCounterRegistry, CashierSession
|
|
)
|
|
import json
|
|
from datetime import timedelta
|
|
from django.utils import timezone
|
|
from django.contrib import messages
|
|
from django.utils.text import slugify
|
|
import openpyxl
|
|
import csv
|
|
|
|
# Forced update to trigger reload
|
|
# Fixed imports to use helpers
|
|
|
|
@login_required
|
|
def index(request):
|
|
"""
|
|
Enhanced Meezan Dashboard View
|
|
"""
|
|
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()
|
|
|
|
site_settings = SystemSetting.objects.first()
|
|
|
|
# --- Charts & Analytics Data ---
|
|
|
|
# 1. Monthly Sales (Last 6 months)
|
|
today = timezone.now().date()
|
|
six_months_ago = today - timedelta(days=180)
|
|
|
|
monthly_sales = Sale.objects.filter(created_at__date__gte=six_months_ago) \
|
|
.annotate(month=TruncMonth('created_at')) \
|
|
.values('month') \
|
|
.annotate(total=Sum('total_amount')) \
|
|
.order_by('month')
|
|
|
|
monthly_labels = []
|
|
monthly_data = []
|
|
|
|
current_lang = get_language()
|
|
|
|
for entry in monthly_sales:
|
|
dt = entry['month']
|
|
# Format: "Jan 2026" or localized equivalent
|
|
monthly_labels.append(date_format(dt, "M Y"))
|
|
monthly_data.append(float(entry['total']))
|
|
|
|
# 2. Daily Sales (Last 7 days)
|
|
last_week = today - timedelta(days=7)
|
|
daily_sales = Sale.objects.filter(created_at__date__gte=last_week) \
|
|
.annotate(day=TruncDate('created_at')) \
|
|
.values('day') \
|
|
.annotate(total=Sum('total_amount')) \
|
|
.order_by('day')
|
|
|
|
chart_labels = []
|
|
chart_data = []
|
|
|
|
# Fill in missing days for a smooth line chart
|
|
days_map = {entry['day']: entry['total'] for entry in daily_sales}
|
|
for i in range(7):
|
|
d = last_week + timedelta(days=i)
|
|
chart_labels.append(date_format(d, "M d")) # "Feb 07"
|
|
chart_data.append(float(days_map.get(d, 0)))
|
|
|
|
# 3. Sales by Category
|
|
category_sales = SaleItem.objects.values(
|
|
'product__category__name_en',
|
|
'product__category__name_ar'
|
|
).annotate(total=Sum('line_total')).order_by('-total')[:5]
|
|
|
|
category_labels = []
|
|
category_data = []
|
|
|
|
for item in category_sales:
|
|
name_en = item['product__category__name_en'] or "Uncategorized"
|
|
name_ar = item['product__category__name_ar'] or name_en
|
|
|
|
label = name_ar if current_lang == 'ar' else name_en
|
|
category_labels.append(label)
|
|
category_data.append(float(item['total']))
|
|
|
|
# 4. Top Selling Products
|
|
top_products = SaleItem.objects.values(
|
|
'product__name_en',
|
|
'product__name_ar'
|
|
).annotate(
|
|
total_qty=Sum('quantity'),
|
|
total_rev=Sum('line_total')
|
|
).order_by('-total_qty')[:5]
|
|
|
|
# 5. Payment Methods
|
|
payment_stats = SalePayment.objects.values('payment_method_name').annotate(count=Count('id'))
|
|
payment_labels = []
|
|
payment_data = []
|
|
|
|
for stat in payment_stats:
|
|
method_name = stat['payment_method_name']
|
|
|
|
# Simple translation/mapping for known methods
|
|
if method_name:
|
|
if method_name.lower() == 'cash':
|
|
label = _('Cash')
|
|
elif method_name.lower() == 'card':
|
|
label = _('Card')
|
|
else:
|
|
label = method_name
|
|
else:
|
|
label = _('Unknown')
|
|
|
|
payment_labels.append(str(label))
|
|
payment_data.append(stat['count'])
|
|
|
|
# --- Inventory Alerts ---
|
|
low_stock_threshold = 10
|
|
low_stock_products = Product.objects.filter(stock_quantity__lte=F('min_stock_level')).select_related('category')[:5]
|
|
low_stock_count = Product.objects.filter(stock_quantity__lte=F('min_stock_level')).count()
|
|
|
|
expired_count = Product.objects.filter(expiry_date__lt=today).count()
|
|
|
|
# --- Recent Transactions ---
|
|
recent_sales = Sale.objects.select_related('customer', 'created_by').order_by('-created_at')[:5]
|
|
|
|
context = {
|
|
'total_products': total_products,
|
|
'total_sales_count': total_sales_count,
|
|
'total_sales_amount': total_sales_amount,
|
|
'total_customers': total_customers,
|
|
'site_settings': site_settings,
|
|
'monthly_labels': json.dumps(monthly_labels),
|
|
'monthly_data': json.dumps(monthly_data),
|
|
'chart_labels': json.dumps(chart_labels),
|
|
'chart_data': json.dumps(chart_data),
|
|
'category_labels': json.dumps(category_labels),
|
|
'category_data': json.dumps(category_data),
|
|
'top_products': top_products,
|
|
'payment_labels': json.dumps(payment_labels),
|
|
'payment_data': json.dumps(payment_data),
|
|
'low_stock_products': low_stock_products,
|
|
'low_stock_count': low_stock_count,
|
|
'expired_count': expired_count,
|
|
'recent_sales': recent_sales,
|
|
}
|
|
return render(request, 'core/index.html', context)
|
|
|
|
@login_required
|
|
def inventory(request):
|
|
products = Product.objects.all().order_by('-id')
|
|
categories = Category.objects.all()
|
|
units = Unit.objects.all()
|
|
|
|
# Filter by Category
|
|
category_id = request.GET.get('category')
|
|
if category_id:
|
|
products = products.filter(category_id=category_id)
|
|
|
|
# Filter by Search
|
|
search_query = request.GET.get('search')
|
|
if search_query:
|
|
products = products.filter(
|
|
Q(name_en__icontains=search_query) |
|
|
Q(name_ar__icontains=search_query) |
|
|
Q(sku__icontains=search_query)
|
|
)
|
|
|
|
paginator = Paginator(products, 25)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {
|
|
'products': page_obj,
|
|
'categories': categories,
|
|
'units': units,
|
|
}
|
|
return render(request, 'core/inventory.html', context)
|
|
|
|
@login_required
|
|
def customers(request):
|
|
customers = Customer.objects.all().order_by('-id')
|
|
paginator = Paginator(customers, 25)
|
|
return render(request, 'core/customers.html', {'customers': paginator.get_page(request.GET.get('page'))})
|
|
|
|
@login_required
|
|
def suppliers(request):
|
|
suppliers = Supplier.objects.all().order_by('-id')
|
|
paginator = Paginator(suppliers, 25)
|
|
return render(request, 'core/suppliers.html', {'suppliers': paginator.get_page(request.GET.get('page'))})
|
|
|
|
@login_required
|
|
def purchases(request):
|
|
purchases = Purchase.objects.all().order_by('-created_at')
|
|
paginator = Paginator(purchases, 25)
|
|
return render(request, 'core/purchases.html', {'purchases': paginator.get_page(request.GET.get('page'))})
|
|
|
|
@login_required
|
|
def purchase_detail(request, pk):
|
|
purchase = get_object_or_404(Purchase, pk=pk)
|
|
return render(request, 'core/purchase_detail.html', {'purchase': purchase, 'settings': SystemSetting.objects.first()})
|
|
|
|
@login_required
|
|
def purchase_create(request):
|
|
# Stub for purchase creation - redirects to list for now
|
|
return redirect('purchases')
|
|
|
|
@login_required
|
|
def add_product(request):
|
|
if request.method == 'POST':
|
|
# Quick add logic for simplified form
|
|
name_en = request.POST.get('name_en')
|
|
sku = request.POST.get('sku')
|
|
price = request.POST.get('price')
|
|
cost_price = request.POST.get('cost_price')
|
|
stock = request.POST.get('stock_quantity')
|
|
category_id = request.POST.get('category')
|
|
|
|
try:
|
|
Product.objects.create(
|
|
name_en=name_en, sku=sku, price=price, cost_price=cost_price,
|
|
stock_quantity=stock, category_id=category_id,
|
|
created_by=request.user
|
|
)
|
|
messages.success(request, 'Product added successfully.')
|
|
except Exception as e:
|
|
messages.error(request, str(e))
|
|
|
|
return redirect('inventory')
|
|
|
|
@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.price = request.POST.get('price')
|
|
product.cost_price = request.POST.get('cost_price')
|
|
product.stock_quantity = request.POST.get('stock_quantity')
|
|
product.min_stock_level = request.POST.get('min_stock_level') or 0
|
|
product.category_id = request.POST.get('category')
|
|
product.save()
|
|
messages.success(request, 'Product updated.')
|
|
return redirect('inventory')
|
|
|
|
# Render edit form if needed, but for now redirecting
|
|
return redirect('inventory')
|
|
|
|
@login_required
|
|
def delete_product(request, pk):
|
|
product = get_object_or_404(Product, pk=pk)
|
|
product.delete()
|
|
messages.success(request, 'Product deleted.')
|
|
return redirect('inventory')
|
|
|
|
@login_required
|
|
def barcode_labels(request):
|
|
# Logic to print barcodes
|
|
return render(request, 'core/barcode_labels.html')
|
|
|
|
@login_required
|
|
def import_products(request):
|
|
# Logic to import from Excel
|
|
return redirect('inventory')
|
|
|
|
@login_required
|
|
def pos(request):
|
|
# Ensure a session is active for this user/device
|
|
# Check if this user has an open session
|
|
# For now, we'll just show the POS
|
|
|
|
products = Product.objects.filter(is_active=True).select_related('category')
|
|
categories = Category.objects.all()
|
|
customers = Customer.objects.all()
|
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
|
settings = SystemSetting.objects.first()
|
|
|
|
context = {
|
|
'products': products,
|
|
'categories': categories,
|
|
'customers': customers,
|
|
'payment_methods': payment_methods,
|
|
'settings': settings,
|
|
}
|
|
return render(request, 'core/pos.html', context)
|
|
|
|
@login_required
|
|
def customer_display(request):
|
|
return render(request, 'core/customer_display.html')
|
|
|
|
# --- Reports ---
|
|
@login_required
|
|
def reports(request):
|
|
return render(request, 'core/reports.html')
|
|
|
|
@login_required
|
|
def customer_statement(request):
|
|
customers = Customer.objects.all()
|
|
selected_customer = None
|
|
transactions = []
|
|
|
|
if request.GET.get('customer'):
|
|
selected_customer = get_object_or_404(Customer, pk=request.GET.get('customer'))
|
|
# Gather sales, payments, etc.
|
|
sales = Sale.objects.filter(customer=selected_customer).annotate(type=models.Value('Sale', output_field=models.CharField()))
|
|
payments = SalePayment.objects.filter(sale__customer=selected_customer).annotate(type=models.Value('Payment', output_field=models.CharField()))
|
|
# Merge and sort by date... (Simplified)
|
|
transactions = list(sales) + list(payments)
|
|
transactions.sort(key=lambda x: x.created_at, reverse=True)
|
|
|
|
return render(request, 'core/customer_statement.html', {'customers': customers, 'selected_customer': selected_customer, 'transactions': transactions})
|
|
|
|
@login_required
|
|
def supplier_statement(request):
|
|
suppliers = Supplier.objects.all()
|
|
return render(request, 'core/supplier_statement.html', {'suppliers': suppliers})
|
|
|
|
@login_required
|
|
def cashflow_report(request):
|
|
sales = Sale.objects.all()
|
|
expenses = Expense.objects.all()
|
|
purchases = Purchase.objects.all()
|
|
if request.GET.get('start_date'):
|
|
sales = sales.filter(created_at__date__gte=request.GET.get('start_date'))
|
|
expenses = expenses.filter(date__gte=request.GET.get('start_date'))
|
|
purchases = purchases.filter(created_at__date__gte=request.GET.get('start_date'))
|
|
if request.GET.get('end_date'):
|
|
sales = sales.filter(created_at__date__lte=request.GET.get('end_date'))
|
|
expenses = expenses.filter(date__lte=request.GET.get('end_date'))
|
|
purchases = purchases.filter(created_at__date__lte=request.GET.get('end_date'))
|
|
total_sales = sales.aggregate(total=Sum('total_amount'))['total'] or 0
|
|
total_expenses = expenses.aggregate(total=Sum('amount'))['total'] or 0
|
|
total_purchases = purchases.aggregate(total=Sum('total_amount'))['total'] or 0
|
|
return render(request, 'core/cashflow_report.html', {'total_sales': total_sales, 'total_expenses': total_expenses, 'total_purchases': total_purchases, 'net_profit': total_sales - total_expenses - total_purchases})
|
|
|
|
@login_required
|
|
def invoice_list(request):
|
|
sales = Sale.objects.all().order_by('-created_at')
|
|
|
|
# Filter by date range
|
|
start_date = request.GET.get('start_date')
|
|
end_date = request.GET.get('end_date')
|
|
if start_date:
|
|
sales = sales.filter(created_at__date__gte=start_date)
|
|
if end_date:
|
|
sales = sales.filter(created_at__date__lte=end_date)
|
|
|
|
# Filter by customer
|
|
customer_id = request.GET.get('customer')
|
|
if customer_id:
|
|
sales = sales.filter(customer_id=customer_id)
|
|
|
|
# Filter by status
|
|
status = request.GET.get('status')
|
|
if status:
|
|
sales = sales.filter(status=status)
|
|
|
|
paginator = Paginator(sales, 25)
|
|
|
|
context = {
|
|
'sales': paginator.get_page(request.GET.get('page')),
|
|
'customers': Customer.objects.all(),
|
|
'payment_methods': PaymentMethod.objects.filter(is_active=True),
|
|
'site_settings': SystemSetting.objects.first(),
|
|
}
|
|
return render(request, 'core/invoices.html', context)
|
|
|
|
@login_required
|
|
def invoice_detail(request, pk):
|
|
return render(request, 'core/invoice_detail.html', {'sale': get_object_or_404(Sale, pk=pk), 'settings': SystemSetting.objects.first()})
|
|
|
|
@login_required
|
|
def invoice_create(request):
|
|
customers = Customer.objects.all()
|
|
products = Product.objects.filter(is_active=True).select_related('category')
|
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
|
site_settings = SystemSetting.objects.first()
|
|
|
|
decimal_places = 2
|
|
if site_settings:
|
|
decimal_places = site_settings.decimal_places
|
|
|
|
context = {
|
|
'customers': customers,
|
|
'products': products,
|
|
'payment_methods': payment_methods,
|
|
'site_settings': site_settings,
|
|
'decimal_places': decimal_places,
|
|
}
|
|
return render(request, 'core/invoice_create.html', context)
|
|
|
|
# --- STUBS & MISSING VIEWS ---
|
|
@login_required
|
|
def quotations(request): return render(request, 'core/quotations.html')
|
|
@login_required
|
|
def quotation_create(request): return redirect('quotations')
|
|
@login_required
|
|
def quotation_detail(request, pk): return redirect('quotations')
|
|
@login_required
|
|
def convert_quotation_to_invoice(request, pk): return redirect('quotations')
|
|
@login_required
|
|
def delete_quotation(request, pk): return redirect('quotations')
|
|
@csrf_exempt
|
|
def create_quotation_api(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def sales_returns(request): return render(request, 'core/sales_returns.html')
|
|
@login_required
|
|
def sale_return_create(request): return redirect('sales_returns')
|
|
@login_required
|
|
def sale_return_detail(request, pk): return redirect('sales_returns')
|
|
@login_required
|
|
def delete_sale_return(request, pk): return redirect('sales_returns')
|
|
@csrf_exempt
|
|
def create_sale_return_api(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def add_purchase_payment(request, pk): return redirect('purchases')
|
|
@login_required
|
|
def delete_purchase(request, pk): return redirect('purchases')
|
|
@login_required
|
|
def purchase_returns(request): return render(request, 'core/purchase_returns.html')
|
|
@login_required
|
|
def purchase_return_create(request): return redirect('purchase_returns')
|
|
@login_required
|
|
def purchase_return_detail(request, pk): return redirect('purchase_returns')
|
|
@login_required
|
|
def delete_purchase_return(request, pk): return redirect('purchase_returns')
|
|
@csrf_exempt
|
|
def create_purchase_return_api(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def export_expenses_excel(request): return redirect('expenses')
|
|
@csrf_exempt
|
|
def update_sale_api(request, pk):
|
|
if request.method != 'POST':
|
|
return JsonResponse({'success': False, 'error': 'Invalid request method'})
|
|
|
|
try:
|
|
sale = Sale.objects.get(pk=pk)
|
|
data = json.loads(request.body)
|
|
|
|
customer_id = data.get('customer_id')
|
|
items = data.get('items', [])
|
|
discount = decimal.Decimal(str(data.get('discount', 0)))
|
|
paid_amount = decimal.Decimal(str(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', '')
|
|
invoice_number = data.get('invoice_number')
|
|
|
|
if not items:
|
|
return JsonResponse({'success': False, 'error': 'No items in sale'})
|
|
|
|
with transaction.atomic():
|
|
# 1. Revert Stock
|
|
for item in sale.items.all():
|
|
product = item.product
|
|
product.stock_quantity += item.quantity
|
|
product.save()
|
|
|
|
# 2. Delete existing items
|
|
sale.items.all().delete()
|
|
|
|
# 3. Update Sale Details
|
|
if customer_id:
|
|
sale.customer_id = customer_id
|
|
else:
|
|
sale.customer = None
|
|
|
|
sale.discount = discount
|
|
sale.notes = notes
|
|
if invoice_number:
|
|
sale.invoice_number = invoice_number
|
|
|
|
if due_date:
|
|
sale.due_date = due_date
|
|
else:
|
|
sale.due_date = None
|
|
|
|
# 4. Create New Items and Deduct Stock
|
|
subtotal = decimal.Decimal(0)
|
|
|
|
for item_data in items:
|
|
product = Product.objects.get(pk=item_data['id'])
|
|
quantity = decimal.Decimal(str(item_data['quantity']))
|
|
price = decimal.Decimal(str(item_data['price']))
|
|
|
|
# Deduct stock
|
|
product.stock_quantity -= quantity
|
|
product.save()
|
|
|
|
line_total = price * quantity
|
|
subtotal += line_total
|
|
|
|
SaleItem.objects.create(
|
|
sale=sale,
|
|
product=product,
|
|
quantity=quantity,
|
|
unit_price=price,
|
|
line_total=line_total
|
|
)
|
|
|
|
sale.subtotal = subtotal
|
|
sale.total_amount = subtotal - discount
|
|
|
|
# 5. Handle Payments
|
|
if payment_type == 'credit':
|
|
sale.status = 'unpaid'
|
|
sale.paid_amount = 0
|
|
sale.balance_due = sale.total_amount
|
|
sale.payments.all().delete()
|
|
|
|
elif payment_type == 'cash':
|
|
sale.status = 'paid'
|
|
sale.paid_amount = sale.total_amount
|
|
sale.balance_due = 0
|
|
|
|
sale.payments.all().delete()
|
|
SalePayment.objects.create(
|
|
sale=sale,
|
|
amount=sale.total_amount,
|
|
payment_method_id=payment_method_id if payment_method_id else None,
|
|
payment_date=timezone.now().date(),
|
|
notes='Full Payment (Edit)'
|
|
)
|
|
|
|
elif payment_type == 'partial':
|
|
sale.paid_amount = paid_amount
|
|
sale.balance_due = sale.total_amount - paid_amount
|
|
if sale.balance_due <= 0:
|
|
sale.status = 'paid'
|
|
sale.balance_due = 0
|
|
else:
|
|
sale.status = 'partial'
|
|
|
|
sale.payments.all().delete()
|
|
SalePayment.objects.create(
|
|
sale=sale,
|
|
amount=paid_amount,
|
|
payment_method_id=payment_method_id if payment_method_id else None,
|
|
payment_date=timezone.now().date(),
|
|
notes='Partial Payment (Edit)'
|
|
)
|
|
|
|
sale.save()
|
|
|
|
return JsonResponse({'success': True, 'sale_id': sale.id})
|
|
|
|
except Sale.DoesNotExist:
|
|
return JsonResponse({'success': False, 'error': 'Sale not found'})
|
|
except Product.DoesNotExist:
|
|
return JsonResponse({'success': False, 'error': 'Product not found'})
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)})
|
|
|
|
@csrf_exempt
|
|
def hold_sale_api(request): return JsonResponse({'success': False})
|
|
@csrf_exempt
|
|
def get_held_sales_api(request): return JsonResponse({'sales': []})
|
|
@csrf_exempt
|
|
def recall_held_sale_api(request, pk): return JsonResponse({'success': False})
|
|
@csrf_exempt
|
|
def delete_held_sale_api(request, pk): return JsonResponse({'success': False})
|
|
@login_required
|
|
def add_customer(request): return redirect('customers')
|
|
@login_required
|
|
def edit_customer(request, pk): return redirect('customers')
|
|
@login_required
|
|
def delete_customer(request, pk): return redirect('customers')
|
|
@csrf_exempt
|
|
def add_customer_ajax(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def add_supplier(request): return redirect('suppliers')
|
|
@login_required
|
|
def edit_supplier(request, pk): return redirect('suppliers')
|
|
@login_required
|
|
def delete_supplier(request, pk): return redirect('suppliers')
|
|
@csrf_exempt
|
|
def add_supplier_ajax(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def suggest_sku(request): return JsonResponse({'sku': '12345'})
|
|
@login_required
|
|
def add_category(request): return redirect('inventory')
|
|
@login_required
|
|
def edit_category(request, pk): return redirect('inventory')
|
|
@login_required
|
|
def delete_category(request, pk): return redirect('inventory')
|
|
@csrf_exempt
|
|
def add_category_ajax(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def import_categories(request): return redirect('inventory')
|
|
@login_required
|
|
def add_unit(request): return redirect('inventory')
|
|
@login_required
|
|
def edit_unit(request, pk): return redirect('inventory')
|
|
@login_required
|
|
def delete_unit(request, pk): return redirect('inventory')
|
|
@csrf_exempt
|
|
def add_unit_ajax(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def add_payment_method(request): return redirect('settings')
|
|
@login_required
|
|
def edit_payment_method(request, pk): return redirect('settings')
|
|
@login_required
|
|
def delete_payment_method(request, pk): return redirect('settings')
|
|
@csrf_exempt
|
|
def add_payment_method_ajax(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def add_loyalty_tier(request): return redirect('settings')
|
|
@login_required
|
|
def edit_loyalty_tier(request, pk): return redirect('settings')
|
|
@login_required
|
|
def delete_loyalty_tier(request, pk): return redirect('settings')
|
|
@csrf_exempt
|
|
def get_customer_loyalty_api(request, pk): return JsonResponse({'success': False})
|
|
@csrf_exempt
|
|
def send_invoice_whatsapp(request): return JsonResponse({'success': False})
|
|
@csrf_exempt
|
|
def test_whatsapp_connection(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def add_device(request): return redirect('settings')
|
|
@login_required
|
|
def edit_device(request, pk): return redirect('settings')
|
|
@login_required
|
|
def delete_device(request, pk): return redirect('settings')
|
|
@login_required
|
|
def lpo_list(request): return render(request, 'core/lpo_list.html')
|
|
@login_required
|
|
def lpo_create(request): return redirect('lpo_list')
|
|
@login_required
|
|
def lpo_detail(request, pk): return redirect('lpo_list')
|
|
@login_required
|
|
def convert_lpo_to_purchase(request, pk): return redirect('lpo_list')
|
|
@login_required
|
|
def lpo_delete(request, pk): return redirect('lpo_list')
|
|
@csrf_exempt
|
|
def create_lpo_api(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def cashier_registry(request): return redirect('settings')
|
|
@login_required
|
|
def cashier_session_list(request): return render(request, 'core/cashier_sessions.html')
|
|
@login_required
|
|
def start_session(request): return redirect('cashier_session_list')
|
|
@login_required
|
|
def close_session(request): return redirect('cashier_session_list')
|
|
@login_required
|
|
def session_detail(request, pk): return redirect('cashier_session_list')
|
|
|
|
@login_required
|
|
def expenses_view(request):
|
|
expenses = Expense.objects.all().select_related('category', 'payment_method', 'created_by').order_by('-date')
|
|
categories = ExpenseCategory.objects.all()
|
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
|
|
|
paginator = Paginator(expenses, 25)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {
|
|
'expenses': page_obj,
|
|
'categories': categories,
|
|
'payment_methods': payment_methods,
|
|
}
|
|
return render(request, 'core/expenses.html', context)
|
|
|
|
@login_required
|
|
def expense_create_view(request):
|
|
if request.method == 'POST':
|
|
try:
|
|
category_id = request.POST.get('category')
|
|
amount = request.POST.get('amount')
|
|
date = request.POST.get('date')
|
|
description = request.POST.get('description')
|
|
payment_method_id = request.POST.get('payment_method')
|
|
|
|
category = get_object_or_404(ExpenseCategory, pk=category_id)
|
|
payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None
|
|
|
|
expense = Expense.objects.create(
|
|
category=category,
|
|
amount=amount,
|
|
date=date or timezone.now().date(),
|
|
description=description,
|
|
payment_method=payment_method,
|
|
created_by=request.user
|
|
)
|
|
|
|
if 'attachment' in request.FILES:
|
|
expense.attachment = request.FILES['attachment']
|
|
expense.save()
|
|
|
|
messages.success(request, _('Expense added successfully.'))
|
|
except Exception as e:
|
|
messages.error(request, _('Error adding expense: ') + str(e))
|
|
|
|
return redirect('expenses')
|
|
|
|
@login_required
|
|
def expense_edit_view(request, pk):
|
|
expense = get_object_or_404(Expense, pk=pk)
|
|
if request.method == 'POST':
|
|
try:
|
|
category_id = request.POST.get('category')
|
|
amount = request.POST.get('amount')
|
|
date = request.POST.get('date')
|
|
description = request.POST.get('description')
|
|
payment_method_id = request.POST.get('payment_method')
|
|
|
|
category = get_object_or_404(ExpenseCategory, pk=category_id)
|
|
payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None
|
|
|
|
expense.category = category
|
|
expense.amount = amount
|
|
expense.date = date or expense.date
|
|
expense.description = description
|
|
expense.payment_method = payment_method
|
|
|
|
if 'attachment' in request.FILES:
|
|
expense.attachment = request.FILES['attachment']
|
|
|
|
expense.save()
|
|
messages.success(request, _('Expense updated successfully.'))
|
|
except Exception as e:
|
|
messages.error(request, _('Error updating expense: ') + str(e))
|
|
|
|
return redirect('expenses')
|
|
|
|
@login_required
|
|
def expense_delete_view(request, pk):
|
|
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):
|
|
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:
|
|
# Update existing category
|
|
category = get_object_or_404(ExpenseCategory, pk=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:
|
|
# Create new category
|
|
ExpenseCategory.objects.create(
|
|
name_en=name_en,
|
|
name_ar=name_ar,
|
|
description=description
|
|
)
|
|
messages.success(request, _('Expense category added successfully.'))
|
|
return redirect('expense_categories')
|
|
|
|
categories = ExpenseCategory.objects.all().order_by('-id')
|
|
return render(request, 'core/expense_categories.html', {'categories': categories})
|
|
|
|
@login_required
|
|
def expense_category_delete_view(request, pk):
|
|
category = get_object_or_404(ExpenseCategory, pk=pk)
|
|
if category.expenses.exists():
|
|
messages.error(request, _('Cannot delete category because it has related expenses.'))
|
|
else:
|
|
category.delete()
|
|
messages.success(request, _('Expense category deleted successfully.'))
|
|
return redirect('expense_categories')
|
|
|
|
@login_required
|
|
def expense_report(request): return render(request, 'core/expense_report.html')
|
|
@login_required
|
|
def customer_payments(request): return redirect('invoices')
|
|
@login_required
|
|
def customer_payment_receipt(request, pk): return redirect('invoices')
|
|
@login_required
|
|
def sale_receipt(request, pk):
|
|
sale = get_object_or_404(Sale, pk=pk)
|
|
settings = SystemSetting.objects.first()
|
|
return render(request, 'core/sale_receipt.html', {
|
|
'sale': sale,
|
|
'settings': settings
|
|
})
|
|
|
|
@login_required
|
|
def edit_invoice(request, pk):
|
|
sale = get_object_or_404(Sale, pk=pk)
|
|
customers = Customer.objects.all()
|
|
products = Product.objects.filter(is_active=True).select_related('category')
|
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
|
site_settings = SystemSetting.objects.first()
|
|
|
|
decimal_places = 2
|
|
if site_settings:
|
|
decimal_places = site_settings.decimal_places
|
|
|
|
# Serialize items for Vue
|
|
cart_items = []
|
|
for item in sale.items.all().select_related('product'):
|
|
cart_items.append({
|
|
'id': item.product.id,
|
|
'name_en': item.product.name_en,
|
|
'name_ar': item.product.name_ar,
|
|
'sku': item.product.sku,
|
|
'price': float(item.unit_price),
|
|
'quantity': float(item.quantity),
|
|
'stock': float(item.product.stock_quantity)
|
|
})
|
|
|
|
cart_json = json.dumps(cart_items)
|
|
|
|
# Get first payment method if exists
|
|
payment_method_id = ""
|
|
first_payment = sale.payments.first()
|
|
if first_payment and first_payment.payment_method:
|
|
payment_method_id = first_payment.payment_method.id
|
|
|
|
context = {
|
|
'sale': sale,
|
|
'customers': customers,
|
|
'products': products,
|
|
'payment_methods': payment_methods,
|
|
'site_settings': site_settings,
|
|
'decimal_places': decimal_places,
|
|
'cart_json': cart_json,
|
|
'payment_method_id': payment_method_id
|
|
}
|
|
return render(request, 'core/invoice_edit.html', context)
|
|
|
|
@login_required
|
|
def add_sale_payment(request, pk): return redirect('invoices')
|
|
@login_required
|
|
def delete_sale(request, pk): return redirect('invoices')
|
|
@login_required
|
|
def supplier_payments(request): return redirect('purchases')
|
|
@login_required
|
|
def settings_view(request):
|
|
settings, created = SystemSetting.objects.get_or_create(id=1)
|
|
|
|
if request.method == 'POST':
|
|
# Business Profile
|
|
settings.business_name = request.POST.get('business_name', '')
|
|
settings.email = request.POST.get('email', '')
|
|
settings.phone = request.POST.get('phone', '')
|
|
settings.vat_number = request.POST.get('vat_number', '')
|
|
settings.registration_number = request.POST.get('registration_number', '')
|
|
settings.address = request.POST.get('address', '')
|
|
|
|
# Financial
|
|
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.allow_zero_stock_sales = request.POST.get('allow_zero_stock_sales') == 'on'
|
|
|
|
# Loyalty
|
|
settings.loyalty_enabled = request.POST.get('loyalty_enabled') == 'on'
|
|
settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100)
|
|
settings.points_per_currency = request.POST.get('points_per_currency', 1.0)
|
|
settings.currency_per_point = request.POST.get('currency_per_point', 0.010)
|
|
|
|
# WhatsApp (Wablas)
|
|
settings.wablas_enabled = request.POST.get('wablas_enabled') == 'on'
|
|
settings.wablas_server_url = request.POST.get('wablas_server_url', '')
|
|
settings.wablas_token = request.POST.get('wablas_token', '')
|
|
settings.wablas_secret_key = request.POST.get('wablas_secret_key', '')
|
|
|
|
# Logo Upload
|
|
if 'logo' in request.FILES:
|
|
settings.logo = request.FILES['logo']
|
|
|
|
settings.save()
|
|
messages.success(request, _('System settings updated successfully.'))
|
|
return redirect('settings')
|
|
|
|
payment_methods = PaymentMethod.objects.all()
|
|
devices = Device.objects.all()
|
|
loyalty_tiers = LoyaltyTier.objects.all()
|
|
|
|
return render(request, 'core/settings.html', {
|
|
'settings': settings,
|
|
'payment_methods': payment_methods,
|
|
'devices': devices,
|
|
'loyalty_tiers': loyalty_tiers,
|
|
})
|
|
|
|
@login_required
|
|
def profile_view(request): return render(request, 'core/profile.html')
|
|
@login_required
|
|
def user_management(request): return render(request, 'core/users.html')
|
|
@csrf_exempt
|
|
def group_details_api(request, pk): return JsonResponse({'success': False})
|
|
@csrf_exempt
|
|
def create_sale_api(request):
|
|
if request.method != 'POST':
|
|
return JsonResponse({'success': False, 'error': 'Invalid request method'})
|
|
|
|
try:
|
|
data = json.loads(request.body)
|
|
|
|
customer_id = data.get('customer_id')
|
|
items = data.get('items', [])
|
|
discount = decimal.Decimal(str(data.get('discount', 0)))
|
|
paid_amount = decimal.Decimal(str(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', '')
|
|
invoice_number = data.get('invoice_number')
|
|
|
|
if not items:
|
|
return JsonResponse({'success': False, 'error': 'No items in sale'})
|
|
|
|
with transaction.atomic():
|
|
customer = None
|
|
if customer_id:
|
|
customer = Customer.objects.get(pk=customer_id)
|
|
|
|
# Calculate totals server-side for security
|
|
subtotal = decimal.Decimal(0)
|
|
vat_amount = decimal.Decimal(0)
|
|
|
|
sale = Sale(
|
|
customer=customer,
|
|
created_by=request.user,
|
|
status='unpaid',
|
|
discount=discount,
|
|
notes=notes,
|
|
invoice_number=invoice_number
|
|
)
|
|
if due_date:
|
|
sale.due_date = due_date
|
|
sale.save()
|
|
|
|
for item in items:
|
|
product = Product.objects.select_for_update().get(pk=item['id'])
|
|
qty = decimal.Decimal(str(item['quantity']))
|
|
unit_price = decimal.Decimal(str(item['price']))
|
|
|
|
# Check stock
|
|
if product.stock_quantity < qty:
|
|
settings = SystemSetting.objects.first()
|
|
if not settings or not settings.allow_zero_stock_sales:
|
|
raise Exception(f"Insufficient stock for {product.name_en}")
|
|
|
|
line_total = unit_price * qty
|
|
line_vat = line_total * (product.vat / 100) if product.vat else 0
|
|
|
|
SaleItem.objects.create(
|
|
sale=sale,
|
|
product=product,
|
|
quantity=qty,
|
|
unit_price=unit_price,
|
|
line_total=line_total
|
|
)
|
|
|
|
product.stock_quantity -= qty
|
|
product.save()
|
|
|
|
subtotal += line_total
|
|
vat_amount += decimal.Decimal(line_vat)
|
|
|
|
sale.subtotal = subtotal
|
|
sale.vat_amount = vat_amount
|
|
sale.total_amount = subtotal + vat_amount - discount
|
|
|
|
if payment_type == 'credit':
|
|
sale.status = 'unpaid'
|
|
elif paid_amount >= sale.total_amount:
|
|
sale.status = 'paid'
|
|
else:
|
|
sale.status = 'partial'
|
|
|
|
sale.save()
|
|
|
|
if paid_amount > 0 and payment_type != 'credit':
|
|
payment_method = None
|
|
if payment_method_id:
|
|
payment_method = PaymentMethod.objects.get(pk=payment_method_id)
|
|
|
|
SalePayment.objects.create(
|
|
sale=sale,
|
|
amount=paid_amount,
|
|
payment_method=payment_method,
|
|
payment_date=timezone.now(),
|
|
created_by=request.user,
|
|
notes=f"Initial payment ({payment_type})"
|
|
)
|
|
|
|
return JsonResponse({'success': True, 'sale_id': sale.id})
|
|
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)})
|
|
|
|
@csrf_exempt
|
|
def create_purchase_api(request): return JsonResponse({'success': False})
|
|
@csrf_exempt
|
|
def search_customers_api(request): return JsonResponse({'customers': []})
|
|
@login_required
|
|
def pos_sync_update(request): return JsonResponse({'success': False})
|
|
@login_required
|
|
def pos_sync_state(request): return JsonResponse({'success': False})
|
|
|
|
@login_required
|
|
def edit_purchase(request, pk):
|
|
purchase = get_object_or_404(Purchase, pk=pk)
|
|
suppliers = Supplier.objects.all()
|
|
products = Product.objects.filter(is_active=True).select_related('category')
|
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
|
site_settings = SystemSetting.objects.first()
|
|
|
|
decimal_places = 2
|
|
if site_settings:
|
|
decimal_places = site_settings.decimal_places
|
|
|
|
# Serialize items for Vue
|
|
cart_items = []
|
|
for item in purchase.items.all().select_related('product'):
|
|
cart_items.append({
|
|
'id': item.product.id,
|
|
'name_en': item.product.name_en,
|
|
'name_ar': item.product.name_ar,
|
|
'sku': item.product.sku,
|
|
'cost_price': float(item.cost_price),
|
|
'quantity': float(item.quantity),
|
|
'stock': float(item.product.stock_quantity)
|
|
})
|
|
|
|
cart_json = json.dumps(cart_items)
|
|
|
|
# Get first payment method if exists
|
|
payment_method_id = ""
|
|
first_payment = purchase.payments.first()
|
|
if first_payment and first_payment.payment_method:
|
|
payment_method_id = first_payment.payment_method.id
|
|
|
|
context = {
|
|
'purchase': purchase,
|
|
'suppliers': suppliers,
|
|
'products': products,
|
|
'payment_methods': payment_methods,
|
|
'site_settings': site_settings,
|
|
'decimal_places': decimal_places,
|
|
'cart_json': cart_json,
|
|
'payment_method_id': payment_method_id
|
|
}
|
|
return render(request, 'core/purchase_edit.html', context)
|
|
|
|
@csrf_exempt
|
|
def update_purchase_api(request, pk):
|
|
if request.method != 'POST':
|
|
return JsonResponse({'success': False, 'error': 'Invalid request method'})
|
|
|
|
try:
|
|
purchase = Purchase.objects.get(pk=pk)
|
|
data = json.loads(request.body)
|
|
|
|
supplier_id = data.get('supplier_id')
|
|
items = data.get('items', [])
|
|
paid_amount = decimal.Decimal(str(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', '')
|
|
invoice_number = data.get('invoice_number')
|
|
|
|
if not items:
|
|
return JsonResponse({'success': False, 'error': 'No items in purchase'})
|
|
|
|
with transaction.atomic():
|
|
# 1. Revert Stock (Subtract what was added)
|
|
for item in purchase.items.all():
|
|
product = item.product
|
|
product.stock_quantity -= item.quantity
|
|
product.save()
|
|
|
|
# 2. Delete existing items
|
|
purchase.items.all().delete()
|
|
|
|
# 3. Update Purchase Details
|
|
if supplier_id:
|
|
purchase.supplier_id = supplier_id
|
|
else:
|
|
purchase.supplier = None
|
|
|
|
purchase.notes = notes
|
|
if invoice_number:
|
|
purchase.invoice_number = invoice_number
|
|
|
|
if due_date:
|
|
purchase.due_date = due_date
|
|
else:
|
|
purchase.due_date = None
|
|
|
|
# 4. Create New Items and Add Stock
|
|
total_amount = decimal.Decimal(0)
|
|
|
|
for item_data in items:
|
|
product = Product.objects.get(pk=item_data['id'])
|
|
quantity = decimal.Decimal(str(item_data['quantity']))
|
|
cost_price = decimal.Decimal(str(item_data['cost_price']))
|
|
|
|
# Add stock
|
|
product.stock_quantity += quantity
|
|
# Update product cost price (optional, but good practice to update to latest)
|
|
product.cost_price = cost_price
|
|
product.save()
|
|
|
|
line_total = cost_price * quantity
|
|
total_amount += line_total
|
|
|
|
PurchaseItem.objects.create(
|
|
purchase=purchase,
|
|
product=product,
|
|
quantity=quantity,
|
|
cost_price=cost_price,
|
|
line_total=line_total
|
|
)
|
|
|
|
purchase.total_amount = total_amount
|
|
|
|
# 5. Handle Payments
|
|
if payment_type == 'credit':
|
|
purchase.status = 'unpaid'
|
|
purchase.paid_amount = 0
|
|
purchase.balance_due = purchase.total_amount
|
|
purchase.payments.all().delete()
|
|
|
|
elif payment_type == 'cash':
|
|
purchase.status = 'paid'
|
|
purchase.paid_amount = purchase.total_amount
|
|
purchase.balance_due = 0
|
|
|
|
purchase.payments.all().delete()
|
|
PurchasePayment.objects.create(
|
|
purchase=purchase,
|
|
amount=purchase.total_amount,
|
|
payment_method_id=payment_method_id if payment_method_id else None,
|
|
payment_date=timezone.now().date(),
|
|
notes='Full Payment (Edit)'
|
|
)
|
|
|
|
elif payment_type == 'partial':
|
|
purchase.paid_amount = paid_amount
|
|
purchase.balance_due = purchase.total_amount - paid_amount
|
|
if purchase.balance_due <= 0:
|
|
purchase.status = 'paid'
|
|
purchase.balance_due = 0
|
|
else:
|
|
purchase.status = 'partial'
|
|
|
|
purchase.payments.all().delete()
|
|
PurchasePayment.objects.create(
|
|
purchase=purchase,
|
|
amount=paid_amount,
|
|
payment_method_id=payment_method_id if payment_method_id else None,
|
|
payment_date=timezone.now().date(),
|
|
notes='Partial Payment (Edit)'
|
|
)
|
|
|
|
purchase.save()
|
|
|
|
return JsonResponse({'success': True, 'purchase_id': purchase.id})
|
|
|
|
except Purchase.DoesNotExist:
|
|
return JsonResponse({'success': False, 'error': 'Purchase not found'})
|
|
except Product.DoesNotExist:
|
|
return JsonResponse({'success': False, 'error': 'Product not found'})
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)}) |