From b66ef8649ee62db95621dbd67b58b33575616436 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 10 Feb 2026 12:23:20 +0000 Subject: [PATCH] Autosave: 20260210-122318 --- core/patch_views.py | 76 +++++++++++++ core/templates/core/inventory.html | 30 +++++ core/templates/core/invoices.html | 27 ++--- core/views.py | 176 ++++++++++++++++++++++++++--- 4 files changed, 281 insertions(+), 28 deletions(-) create mode 100644 core/patch_views.py diff --git a/core/patch_views.py b/core/patch_views.py new file mode 100644 index 0000000..4d0eb9e --- /dev/null +++ b/core/patch_views.py @@ -0,0 +1,76 @@ +@login_required +def send_invoice_whatsapp(request): + 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}. +" + f"Total: {sale.total_amount} +" + f"View Invoice: {receipt_url} +" + 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 diff --git a/core/templates/core/inventory.html b/core/templates/core/inventory.html index 2411165..305cfe4 100644 --- a/core/templates/core/inventory.html +++ b/core/templates/core/inventory.html @@ -358,6 +358,36 @@ + + {% empty %} {% trans "No categories found." %} diff --git a/core/templates/core/invoices.html b/core/templates/core/invoices.html index e9f57ad..30d4f09 100644 --- a/core/templates/core/invoices.html +++ b/core/templates/core/invoices.html @@ -30,6 +30,13 @@
+
+ +
+ + +
+
@@ -49,16 +56,7 @@ {% endfor %}
-
- - -
-
+
{% trans "Reset" %}
@@ -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 @@
- {% for method in payment_methods %} {% endfor %} @@ -202,4 +203,4 @@
-{% 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