+
@@ -88,7 +86,10 @@
{{ sale.invoice_number|default:sale.id }}
{{ sale.created_at|date:"Y-m-d" }} |
-
{{ sale.customer.name|default:_("Guest") }} |
+
+ {{ sale.customer.name|default:_("Guest") }}
+ {{ sale.customer.phone|default:"" }}
+ |
{{ site_settings.currency_symbol }}{{ sale.total_amount|floatformat:3 }} |
@@ -147,7 +148,7 @@
-
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/core/views.py b/core/views.py
index ce2fe58..5463962 100644
--- a/core/views.py
+++ b/core/views.py
@@ -15,6 +15,9 @@ import json
import decimal
import datetime
import logging
+import base64
+import os
+from django.conf import settings as django_settings
from .models import (
SystemSetting, Customer, Supplier, Product, Category, Unit,
@@ -28,7 +31,7 @@ from .forms import (
SystemSettingForm, CustomerForm, SupplierForm, ProductForm, CategoryForm,
UnitForm, ExpenseForm, CashierSessionStartForm, CashierSessionCloseForm
)
-from .utils import number_to_words_en
+from .utils import number_to_words_en, send_whatsapp_message, send_whatsapp_document
from .views_import import *
logger = logging.getLogger(__name__)
@@ -67,7 +70,8 @@ def index(request):
# 3. Charts: Monthly Sales (Last 12 months)
last_12_months = timezone.now() - datetime.timedelta(days=365)
- monthly_sales = (Sale.objects.filter(created_at__gte=last_12_months)
+ monthly_sales = (
+ Sale.objects.filter(created_at__gte=last_12_months)
.annotate(month=TruncMonth('created_at'))
.values('month')
.annotate(total=Sum('total_amount'))
@@ -78,7 +82,8 @@ def index(request):
# 4. Charts: Daily Sales (Last 7 days)
last_7_days = timezone.now() - datetime.timedelta(days=7)
- daily_sales = (Sale.objects.filter(created_at__gte=last_7_days)
+ daily_sales = (
+ Sale.objects.filter(created_at__gte=last_7_days)
.annotate(day=TruncDay('created_at'))
.values('day')
.annotate(total=Sum('total_amount'))
@@ -88,15 +93,17 @@ def index(request):
chart_data = [float(d['total']) for d in daily_sales]
# 5. Category Distribution
- category_dist = (SaleItem.objects.values('product__category__name_en', 'product__category__name_ar')
+ category_dist = (
+ SaleItem.objects.values('product__category__name_en', 'product__category__name_ar')
.annotate(total=Sum('line_total'))
.order_by('-total')[:5])
-
+
category_labels = [c['product__category__name_en'] or 'Uncategorized' for c in category_dist]
category_data = [float(c['total']) for c in category_dist]
# 6. Payment Methods
- payment_dist = (SalePayment.objects.values('payment_method__name_en')
+ payment_dist = (
+ SalePayment.objects.values('payment_method__name_en')
.annotate(total=Sum('amount'))
.order_by('-total'))
@@ -381,6 +388,15 @@ def invoice_list(request):
status = request.GET.get('status')
if status:
sales = sales.filter(status=status)
+
+ # NEW: Search functionality
+ query = request.GET.get('q')
+ if query:
+ sales = sales.filter(
+ Q(customer__name__icontains=query) |
+ Q(customer__phone__icontains=query) |
+ Q(invoice_number__icontains=query)
+ )
paginator = Paginator(sales, 25)
@@ -395,6 +411,7 @@ def invoice_list(request):
'customers': Customer.objects.all(),
'payment_methods': PaymentMethod.objects.filter(is_active=True),
'site_settings': settings,
+ 'query': query, # Pass query back to template
}
return render(request, 'core/invoices.html', context)
@@ -518,7 +535,7 @@ def sale_receipt(request, pk):
'settings': settings
})
-# --- Quotations ---
+# ---Quotations ---
@login_required
def quotations(request):
@@ -871,10 +888,10 @@ def cashflow_report(request):
@login_required
def add_product(request):
if request.method == 'POST':
- form = ProductForm(request.POST, request.FILES)
+ form = ProductForm(request.POST, request.FILES, instance=product)
if form.is_valid():
form.save()
- messages.success(request, _("Product added."))
+ messages.success(request, _("Product updated."))
return redirect(reverse('inventory') + '#items')
return redirect('inventory')
@@ -916,11 +933,20 @@ def add_category(request):
@login_required
def edit_category(request, pk):
- return redirect('inventory')
+ 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.save()
+ messages.success(request, _("Category updated."))
+ return redirect(reverse('inventory') + '#categories-list')
@login_required
def delete_category(request, pk):
- return redirect('inventory')
+ category = get_object_or_404(Category, pk=pk)
+ category.delete()
+ messages.success(request, _("Category deleted."))
+ return redirect(reverse('inventory') + '#categories-list')
@login_required
def add_unit(request):
@@ -1046,7 +1072,76 @@ def test_whatsapp_connection(request):
@login_required
def send_invoice_whatsapp(request):
- return JsonResponse({'success': True})
+ if request.method != 'POST':
+ return JsonResponse({'success': False, 'error': 'Method not allowed'})
+
+ try:
+ # Handle JSON payload
+ data = json.loads(request.body)
+ sale_id = data.get('sale_id')
+ phone = data.get('phone')
+ pdf_data = data.get('pdf_data') # Base64 string
+ except json.JSONDecodeError:
+ # Fallback to Form Data
+ sale_id = request.POST.get('sale_id')
+ phone = request.POST.get('phone')
+ pdf_data = None
+
+ if not sale_id:
+ return JsonResponse({'success': False, 'error': 'Sale ID missing'})
+
+ sale = get_object_or_404(Sale, pk=sale_id)
+
+ if not phone:
+ if sale.customer and sale.customer.phone:
+ phone = sale.customer.phone
+ else:
+ return JsonResponse({'success': False, 'error': 'Phone number missing'})
+
+ try:
+ # If PDF data is present, save and send document
+ if pdf_data:
+ # Remove header if present (data:application/pdf;base64,)
+ if ',' in pdf_data:
+ pdf_data = pdf_data.split(',')[1]
+
+ file_data = base64.b64decode(pdf_data)
+ dir_path = os.path.join(django_settings.MEDIA_ROOT, 'temp_invoices')
+ os.makedirs(dir_path, exist_ok=True)
+
+ filename = f"invoice_{sale.id}_{int(timezone.now().timestamp())}.pdf"
+ file_path = os.path.join(dir_path, filename)
+
+ with open(file_path, 'wb') as f:
+ f.write(file_data)
+
+ # Construct URL
+ file_url = request.build_absolute_uri(django_settings.MEDIA_URL + 'temp_invoices/' + filename)
+
+ success, response_msg = send_whatsapp_document(phone, file_url, caption=f"Invoice #{sale.invoice_number or sale.id}")
+
+ else:
+ # Fallback to Text Link
+ receipt_url = request.build_absolute_uri(reverse('sale_receipt', args=[sale.pk]))
+
+ message = (
+ f"Hello {sale.customer.name if sale.customer else 'Guest'},
+ f"Here is your invoice #{sale.invoice_number or sale.id}.\n"
+ f"Total: {sale.total_amount}\n"
+ f"View Invoice: {receipt_url}\n"
+ f"Thank you for your business!"
+ )
+
+ success, response_msg = send_whatsapp_message(phone, message)
+
+ if success:
+ return JsonResponse({'success': True, 'message': response_msg})
+ else:
+ return JsonResponse({'success': False, 'error': response_msg})
+
+ except Exception as e:
+ logger.error(f"WhatsApp Error: {e}")
+ return JsonResponse({'success': False, 'error': str(e)}) # Changed to str(e) for clarity
# --- LPO ---
@login_required
@@ -1212,8 +1307,57 @@ def create_sale_api(request):
@csrf_exempt
@login_required
def update_sale_api(request, pk):
- # Simplified update stub
- return JsonResponse({'success': True})
+ if request.method != 'POST':
+ return JsonResponse({'success': False, 'error': 'Invalid request'})
+
+ try:
+ data = json.loads(request.body)
+ sale = get_object_or_404(Sale, pk=pk)
+
+ with transaction.atomic():
+ # 1. Restore Stock from OLD Items
+ for item in sale.items.all():
+ Product.objects.filter(pk=item.product_id).update(stock_quantity=F('stock_quantity') + item.quantity)
+
+ # 2. Delete OLD Items
+ sale.items.all().delete()
+
+ # 3. Update Sale Details
+ sale.customer_id = data.get('customer_id') or None
+ sale.total_amount = data.get('total_amount', 0)
+ sale.discount = data.get('discount', 0)
+ sale.notes = data.get('notes', '')
+ sale.invoice_number = data.get('invoice_number', sale.invoice_number)
+ sale.payment_type = data.get('payment_type', sale.payment_type)
+ sale.paid_amount = data.get('paid_amount', sale.paid_amount)
+ if data.get('due_date'):
+ sale.due_date = data.get('due_date')
+ sale.save()
+
+ # 4. Create NEW Items and Deduct Stock
+ for item_data in data.get('items', []):
+ qty = decimal.Decimal(str(item_data['quantity']))
+ price = decimal.Decimal(str(item_data['price']))
+ product_id = item_data['id']
+
+ SaleItem.objects.create(
+ sale=sale,
+ product_id=product_id,
+ quantity=qty,
+ unit_price=price,
+ line_total=qty * price
+ )
+
+ # Deduct Stock
+ Product.objects.filter(pk=product_id).update(stock_quantity=F('stock_quantity') - qty)
+
+ # 5. Handle Payment Update
+ sale.update_balance()
+
+ return JsonResponse({'success': True})
+ except Exception as e:
+ logger.error(f"Update Sale Error: {e}")
+ return JsonResponse({'success': False, 'error': str(e)})
@csrf_exempt
@login_required
@@ -1333,7 +1477,9 @@ def add_customer_ajax(request):
@login_required
def search_customers_api(request):
query = request.GET.get('q', '')
- customers = Customer.objects.filter(name__icontains=query).values('id', 'name', 'phone')[:10]
+ customers = Customer.objects.filter(
+ Q(name__icontains=query) | Q(phone__icontains=query)
+ ).values('id', 'name', 'phone')[:10]
return JsonResponse({'results': list(customers)})
@csrf_exempt
|