diff --git a/assets/pasted-20260206-043257-ab1cccfb.png b/assets/pasted-20260206-043257-ab1cccfb.png new file mode 100644 index 0000000..5af04b9 Binary files /dev/null and b/assets/pasted-20260206-043257-ab1cccfb.png differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index b46e844..91bc8dc 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 64ce50c..1a273ba 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 9c49e09..1bdd3b4 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 6f485b3..2c59616 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ee499ae..512e269 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index ee430b6..b1fa1f8 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/core/customer_display.html b/core/templates/core/customer_display.html new file mode 100644 index 0000000..09bcef1 --- /dev/null +++ b/core/templates/core/customer_display.html @@ -0,0 +1,277 @@ +{% load static i18n l10n %} + + + + + + {% trans "Customer Display" %} | {{ settings.business_name }} + + + + + + + + + +
+
+ {% if settings.logo %} + Logo + {% else %} + + {% endif %} +

{{ settings.business_name }}

+

{% trans "Welcome! We are ready to serve you." %}

+
+
+ + +
+ +
+

{% trans "Your Order" %}

+
+ +
+
+ + +
+
+ {% if settings.logo %} + Logo + {% endif %} +
{{ settings.business_name }}
+
+ + +
+
+ {% trans "Total" %} +
+
{{ settings.currency_symbol }} 0.000
+ +
+ {% trans "Items" %}: 0 + {% trans "Tax" %}: 0.000 +
+
+
+
+ + +
Init (v3)...
+ + + + \ No newline at end of file diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html index 02cde53..dcd3346 100644 --- a/core/templates/core/pos.html +++ b/core/templates/core/pos.html @@ -128,6 +128,14 @@

{% trans "Point of Sale" %}

+
+ + +
@@ -571,6 +579,7 @@ cart = []; document.getElementById('discountInput').value = 0; renderCart(); + broadcastClear(); } } @@ -583,6 +592,7 @@ document.getElementById('subtotalAmount').innerText = `${currency} ${formatAmount(subtotal)}`; document.getElementById('taxAmount').innerText = `${currency} ${formatAmount(totalVat)}`; document.getElementById('totalAmount').innerText = `${currency} ${formatAmount(total)}`; + broadcastCartUpdate(); } function renderCart() { @@ -826,6 +836,7 @@ customerLoyalty = null; document.getElementById('loyaltyInfo').classList.add('d-none'); renderCart(); + broadcastClear(); // Show receipt modal const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal')); @@ -1046,6 +1057,7 @@ document.getElementById('customerSearchInput').value = data.customer_name || ""; document.getElementById('clearCustomerBtn').style.display = data.customer_id ? 'block' : 'none'; renderCart(); + broadcastClear(); onCustomerChange(); updateHeldCount(); @@ -1182,6 +1194,68 @@ document.addEventListener('click', function(e) { container.classList.add('d-none'); } }); + + // Customer Display Logic + let customerDisplayWindow = null; + const displayChannel = new BroadcastChannel('pos_channel'); + + function openCustomerDisplay() { + if (customerDisplayWindow && !customerDisplayWindow.closed) { + customerDisplayWindow.focus(); + } else { + customerDisplayWindow = window.open('{% url "customer_display" %}', 'CustomerDisplay', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no'); + setTimeout(broadcastCartUpdate, 1000); + } + } + + // NEW: Listen for new clients requesting state + displayChannel.onmessage = (event) => { + if (event.data && event.data.type === 'new_client') { + console.log('[POS] New Client Handshake Received'); + broadcastCartUpdate(); + } + }; + + function broadcastCartUpdate() { + const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); + const totalVat = cart.reduce((acc, item) => acc + (item.line_total * (item.vat_rate / 100)), 0); + const discount = parseFloat(document.getElementById('discountInput').value) || 0; + const total = Math.max(0, subtotal + totalVat - discount); + + const msg = { + type: 'update_cart', + timestamp: Date.now(), + cart: cart, + totals: { + subtotal: subtotal, + tax: totalVat, + discount: discount, + total: total + } + }; + + // 1. BroadcastChannel + try { displayChannel.postMessage(msg); } catch(e) { console.error('BC Error:', e); } + + // 2. Direct Window + if (customerDisplayWindow && !customerDisplayWindow.closed) { + customerDisplayWindow.postMessage(msg, '*'); + } + + // 3. LocalStorage Fallback (triggers 'storage' event in other tabs) + localStorage.setItem('pos_cart_update', JSON.stringify(msg)); + console.log('[POS] Cart Broadcast Sent'); + } + + function broadcastClear() { + const msg = { type: 'clear' }; + displayChannel.postMessage(msg); + if (customerDisplayWindow && !customerDisplayWindow.closed) { + customerDisplayWindow.postMessage(msg, '*'); + } + localStorage.setItem('pos_cart_update', JSON.stringify(msg)); + } + {% endlocalize %} {% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 06d1e02..d37296f 100644 --- a/core/urls.py +++ b/core/urls.py @@ -5,6 +5,7 @@ urlpatterns = [ path('', views.index, name='index'), path('inventory/', views.inventory, name='inventory'), path('pos/', views.pos, name='pos'), + path('pos/display/', views.customer_display, name='customer_display'), path('customers/', views.customers, name='customers'), path('suppliers/', views.suppliers, name='suppliers'), path('purchases/', views.purchases, name='purchases'), diff --git a/core/views.py b/core/views.py index 75e987f..a0dcf28 100644 --- a/core/views.py +++ b/core/views.py @@ -484,6 +484,166 @@ def create_sale_api(request): 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"), + 'subtotal': float(sale.subtotal), + 'vat_amount': float(sale.vat_amount), + 'total': float(sale.total_amount), + 'discount': float(sale.discount), + 'paid': float(sale.paid_amount), + 'balance': float(sale.balance_due), + 'customer_name': sale.customer.name if sale.customer else 'Guest', + '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) +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', []) + + # Retrieve amounts + subtotal = data.get('subtotal', 0) + vat_amount = data.get('vat_amount', 0) + 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, + subtotal=subtotal, + vat_amount=vat_amount, + 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 @@ -2423,4 +2583,12 @@ def search_customers_api(request): ).values('id', 'name', 'phone')[:20] else: customers = [] - return JsonResponse({'results': list(customers)}) \ No newline at end of file + return JsonResponse({'results': list(customers)}) + +@login_required +def customer_display(request): + """ + Render the Customer Facing Display screen. + """ + settings = SystemSetting.objects.first() + return render(request, "core/customer_display.html", {"settings": settings}) \ No newline at end of file diff --git a/hr/__pycache__/admin.cpython-311.pyc b/hr/__pycache__/admin.cpython-311.pyc index abc19b1..e1705ac 100644 Binary files a/hr/__pycache__/admin.cpython-311.pyc and b/hr/__pycache__/admin.cpython-311.pyc differ diff --git a/hr/__pycache__/views.cpython-311.pyc b/hr/__pycache__/views.cpython-311.pyc index 756eef3..ac0a16c 100644 Binary files a/hr/__pycache__/views.cpython-311.pyc and b/hr/__pycache__/views.cpython-311.pyc differ