diff --git a/apply_patch.py b/apply_patch.py new file mode 100644 index 0000000..1412fb5 --- /dev/null +++ b/apply_patch.py @@ -0,0 +1,58 @@ + +import re + +with open('core/views.py', 'r') as f: + content = f.read() + +with open('core/patch_views_vat.py', 'r') as f: + new_func = f.read() + +# Regex to find the function definition +# It starts with @csrf_exempt\ndef create_sale_api(request): +# And ends before the next function definition (which likely starts with @ or def) +pattern = r"@csrf_exempt\s+def create_sale_api(request):.*?return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)" + +# Note: The pattern needs to match the indentation and multiline content. +# Since regex for code blocks is tricky, I will use a simpler approach: +# 1. Read the file lines. +# 2. Find start line of create_sale_api. +# 3. Find the end line (start of next function or end of file). +# 4. Replace lines. + +lines = content.splitlines() +start_index = -1 +end_index = -1 + +for i, line in enumerate(lines): + if line.strip() == "def create_sale_api(request):": + # Check if previous line is decorator + if i > 0 and lines[i-1].strip() == "@csrf_exempt": + start_index = i - 1 + else: + start_index = i + break + +if start_index != -1: + # Find the next function or end + # We look for next line starting with 'def ' or '@' at top level + for i in range(start_index + 1, len(lines)): + if lines[i].startswith("def ") or lines[i].startswith("@"): + end_index = i + break + if end_index == -1: + end_index = len(lines) + + # Replace + new_lines = new_func.splitlines() + # Ensure new lines have correct indentation if needed (but views.py is top level mostly) + + # We need to preserve the imports and structure. + # The new_func is complete. + + final_lines = lines[:start_index] + new_lines + lines[end_index:] + + with open('core/views.py', 'w') as f: + f.write('\n'.join(final_lines)) + print("Successfully patched create_sale_api") +else: + print("Could not find create_sale_api function") diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index d3ebbac..6f485b3 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 1ab259a..ee430b6 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0025_sale_subtotal_sale_vat_amount.py b/core/migrations/0025_sale_subtotal_sale_vat_amount.py new file mode 100644 index 0000000..8a51a01 --- /dev/null +++ b/core/migrations/0025_sale_subtotal_sale_vat_amount.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-02-05 12:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0024_device'), + ] + + operations = [ + migrations.AddField( + model_name='sale', + name='subtotal', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Subtotal'), + ), + migrations.AddField( + model_name='sale', + name='vat_amount', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='VAT Amount'), + ), + ] diff --git a/core/migrations/__pycache__/0025_sale_subtotal_sale_vat_amount.cpython-311.pyc b/core/migrations/__pycache__/0025_sale_subtotal_sale_vat_amount.cpython-311.pyc new file mode 100644 index 0000000..75a16e0 Binary files /dev/null and b/core/migrations/__pycache__/0025_sale_subtotal_sale_vat_amount.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 886a0a8..41dcb05 100644 --- a/core/models.py +++ b/core/models.py @@ -147,6 +147,8 @@ class Sale(models.Model): customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales") quotation = models.ForeignKey('Quotation', on_delete=models.SET_NULL, null=True, blank=True, related_name="converted_sale") invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True) + subtotal = models.DecimalField(_("Subtotal"), max_digits=15, decimal_places=3, default=0) + vat_amount = models.DecimalField(_("VAT Amount"), max_digits=15, decimal_places=3, default=0) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0) balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0) @@ -420,4 +422,4 @@ def create_user_profile(sender, instance, created, **kwargs): def save_user_profile(sender, instance, **kwargs): if not hasattr(instance, 'profile'): UserProfile.objects.create(user=instance) - instance.profile.save() + instance.profile.save() \ No newline at end of file diff --git a/core/patch_views_vat.py b/core/patch_views_vat.py new file mode 100644 index 0000000..a883edc --- /dev/null +++ b/core/patch_views_vat.py @@ -0,0 +1,161 @@ +@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', []) + + # 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 + 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) \ No newline at end of file diff --git a/core/templates/core/invoice_create.html b/core/templates/core/invoice_create.html index ed6b6d7..5bfd209 100644 --- a/core/templates/core/invoice_create.html +++ b/core/templates/core/invoice_create.html @@ -60,6 +60,7 @@ {% trans "Product" %} {% trans "Unit Price" %} + {% trans "VAT %" %} {% trans "Quantity" %} {% trans "Total" %} @@ -72,10 +73,13 @@
[[ item.sku ]]
- + - + + + + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] @@ -83,7 +87,7 @@ - + {% trans "Search and add products to this invoice." %} @@ -105,10 +109,15 @@ [[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]] +
+ {% trans "VAT" %} + [[ currencySymbol ]][[ totalVat.toFixed(decimalPlaces) ]] +
+
{% trans "Discount" %}
- +
@@ -184,6 +193,7 @@ name_ar: "{{ p.name_ar|escapejs }}", sku: "{{ p.sku|escapejs }}", price: {{ p.price|default:0 }}, + vat: {{ p.vat|default:site_settings.tax_rate|default:0 }}, stock: {{ p.stock_quantity|default:0 }} }, {% endfor %} @@ -208,8 +218,11 @@ subtotal() { return this.cart.reduce((total, item) => total + (item.price * item.quantity), 0); }, + totalVat() { + return this.cart.reduce((total, item) => total + (item.price * item.quantity * (item.vat_rate / 100)), 0); + }, grandTotal() { - return Math.max(0, this.subtotal - this.discount); + return Math.max(0, this.subtotal + this.totalVat - this.discount); } }, methods: { @@ -235,6 +248,7 @@ name_en: product.name_en, sku: product.sku, price: product.price, + vat_rate: product.vat, quantity: 1 }); } @@ -264,6 +278,8 @@ price: item.price, line_total: item.price * item.quantity })), + subtotal: this.subtotal, + vat_amount: this.totalVat, total_amount: this.grandTotal, discount: this.discount, paid_amount: actualPaidAmount, @@ -300,4 +316,4 @@ }).mount('#saleApp'); {% endlocalize %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/invoice_detail.html b/core/templates/core/invoice_detail.html index 058b6dd..cbd00db 100644 --- a/core/templates/core/invoice_detail.html +++ b/core/templates/core/invoice_detail.html @@ -138,7 +138,15 @@
{% trans "Subtotal" %}
المجموع الفرعي
- {{ settings.currency_symbol }}{{ sale.total_amount|add:sale.discount|floatformat:3 }} + {{ settings.currency_symbol }}{{ sale.subtotal|floatformat:3 }} + + + + +
{% trans "VAT" %}
+
الضريبة
+ + {{ settings.currency_symbol }}{{ sale.vat_amount|floatformat:3 }} {% if sale.discount > 0 %} diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html index 055849e..02cde53 100644 --- a/core/templates/core/pos.html +++ b/core/templates/core/pos.html @@ -146,7 +146,7 @@
{% for product in products %}
-
+
{% if product.image %} {{ product.name_en }} {% else %} @@ -224,6 +224,10 @@
{% trans "Subtotal" %} {{ site_settings.currency_symbol }}0.000 +
+
+ {% trans "VAT" %} + {{ site_settings.currency_symbol }}0.000
{% trans "Discount" %} @@ -443,6 +447,9 @@ Date: التاريخ:
+
+ Customer: +
@@ -457,8 +464,20 @@
-
- TOTAL / المجموع +
+ Subtotal / المجموع الفرعي + +
+
+ VAT / الضريبة + +
+
+ Discount / الخصم + +
+
+ TOTAL / المجموع النهائي
@@ -514,7 +533,7 @@ document.body.classList.toggle('cart-open'); } - function addToCart(id, nameEn, nameAr, price) { + function addToCart(id, nameEn, nameAr, price, vatRate) { const existing = cart.find(item => item.id === id); if (existing) { existing.quantity += 1; @@ -525,6 +544,7 @@ name_en: nameEn, name_ar: nameAr, price, + vat_rate: vatRate, quantity: 1, line_total: price }); @@ -556,10 +576,12 @@ function updateTotals() { 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 - discount); + const total = Math.max(0, subtotal + totalVat - discount); document.getElementById('subtotalAmount').innerText = `${currency} ${formatAmount(subtotal)}`; + document.getElementById('taxAmount').innerText = `${currency} ${formatAmount(totalVat)}`; document.getElementById('totalAmount').innerText = `${currency} ${formatAmount(total)}`; } @@ -612,8 +634,9 @@ if (cart.length === 0) return; 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 totalAmount = Math.max(0, subtotal - discount); + const totalAmount = Math.max(0, subtotal + totalVat - discount); document.getElementById('modalTotalAmount').innerText = `${currency} ${formatAmount(totalAmount)}`; document.getElementById('cashReceivedInput').value = ''; @@ -686,8 +709,9 @@ // Update Total Payable 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 totalAmount = Math.max(0, subtotal - discount - value); + const totalAmount = Math.max(0, subtotal + totalVat - discount - value); document.getElementById('modalTotalAmount').innerText = `${currency} ${formatAmount(totalAmount)}`; calculateBalance(); @@ -714,11 +738,12 @@ function setExactAmount() { 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 loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0; const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 0; - const totalAmount = Math.max(0, subtotal - discount - loyaltyDiscount); + const totalAmount = Math.max(0, subtotal + totalVat - discount - loyaltyDiscount); document.getElementById('cashReceivedInput').value = totalAmount.toFixed(decimalPlaces); calculateBalance(); @@ -731,11 +756,12 @@ function calculateBalance() { 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 loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0; const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 0; - const totalAmount = Math.max(0, subtotal - discount - loyaltyDiscount); + const totalAmount = Math.max(0, subtotal + totalVat - discount - loyaltyDiscount); const received = parseFloat(document.getElementById('cashReceivedInput').value) || 0; const balance = Math.max(0, received - totalAmount); @@ -751,16 +777,19 @@ confirmBtn.innerText = '{% trans "Processing..." %}'; 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 loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0; const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 0; - const totalAmount = Math.max(0, subtotal - discount - loyaltyDiscount); + const totalAmount = Math.max(0, subtotal + totalVat - discount - loyaltyDiscount); const data = { customer_id: document.getElementById('customerSelect').value, payment_method_id: currentPaymentType === 'cash' ? selectedPaymentMethodId : null, items: cart, + subtotal: subtotal, + vat_amount: totalVat, total_amount: totalAmount, paid_amount: currentPaymentType === 'cash' ? totalAmount : 0, discount: discount, @@ -836,7 +865,8 @@ document.getElementById('inv-id-ar').innerText = data.sale.id; document.getElementById('inv-date').innerText = data.sale.created_at; document.getElementById('inv-date-ar').innerText = data.sale.created_at; - + document.getElementById('inv-customer').innerText = data.sale.customer_name || 'Guest'; + let itemsHtml = ''; data.sale.items.forEach(item => { itemsHtml += ` @@ -851,6 +881,10 @@ `; }); document.getElementById('inv-items').innerHTML = itemsHtml; + + document.getElementById('inv-subtotal').innerText = data.business.currency + ' ' + formatAmount(data.sale.subtotal); + document.getElementById('inv-vat').innerText = data.business.currency + ' ' + formatAmount(data.sale.vat_amount); + document.getElementById('inv-discount').innerText = data.business.currency + ' ' + formatAmount(data.sale.discount); document.getElementById('inv-total').innerText = data.business.currency + ' ' + formatAmount(data.sale.total); } @@ -906,8 +940,9 @@ if (cart.length === 0) return; 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 totalAmount = Math.max(0, subtotal - discount); + const totalAmount = Math.max(0, subtotal + totalVat - discount); const data = { customer_id: document.getElementById('customerSelect').value, diff --git a/core/views.py b/core/views.py index 8c556ec..75e987f 100644 --- a/core/views.py +++ b/core/views.py @@ -426,7 +426,12 @@ def create_sale_api(request): 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') @@ -456,6 +461,8 @@ def create_sale_api(request): 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), @@ -552,9 +559,13 @@ def create_sale_api(request): '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, @@ -568,10 +579,6 @@ def create_sale_api(request): }) 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: