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) ]]
+
+
@@ -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 %}

{% 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:
+
-
-
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: