From 9dfa03d69c2275881561fd7907fc18a203f595c8 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 5 Feb 2026 13:09:11 +0000 Subject: [PATCH] adding vat --- apply_patch.py | 58 +++++++ core/__pycache__/models.cpython-311.pyc | Bin 39067 -> 39301 bytes core/__pycache__/views.cpython-311.pyc | Bin 134089 -> 134414 bytes .../0025_sale_subtotal_sale_vat_amount.py | 23 +++ ...e_subtotal_sale_vat_amount.cpython-311.pyc | Bin 0 -> 1032 bytes core/models.py | 4 +- core/patch_views_vat.py | 161 ++++++++++++++++++ core/templates/core/invoice_create.html | 28 ++- core/templates/core/invoice_detail.html | 10 +- core/templates/core/pos.html | 59 +++++-- core/views.py | 15 +- 11 files changed, 334 insertions(+), 24 deletions(-) create mode 100644 apply_patch.py create mode 100644 core/migrations/0025_sale_subtotal_sale_vat_amount.py create mode 100644 core/migrations/__pycache__/0025_sale_subtotal_sale_vat_amount.cpython-311.pyc create mode 100644 core/patch_views_vat.py 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 d3ebbacdfe0f317f517e2bd689fc66b3866dc57e..6f485b3b3739fc2506d1ec0c61459afa803927b4 100644 GIT binary patch delta 2382 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 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 0000000000000000000000000000000000000000..75a16e07ec964217520dab7a2032e7a79269cf4e GIT binary patch literal 1032 zcmah{zi-n(6uz?^$958+77{}#6p13T)D?oQLW+byLQn}6I(RubzH6{+$H6~BVL%x= zFjnf&|Db^Az@Na#m`WL{n~<2gxoW3QymR7Kw3WE?J%8W5d(ZEE=P!+hfnfaJ+VDO} z2>lVH9F++;S_H6-D55w(A+~WS*%C$)dWNX9fvC)|FPA|(fWDtHTc*k~YE}+}yD4!a z^g6MVdQmswY8X)#ByJfg-n9ui`U+qhF=S(kY>DCywq*%1#V?D3PgAj@`qfDcq6)20 zl~#AP9R-SjH6FYAzeQchV_cf2_P;zG$<}Bc+G}wAW!V=rskl+hcx_?f!9zk>-*XvP z5+`6>OR{zvrA}}l1d2g#bZ`|6*On)aroyjK=6azMkY3=pEa6qkR-G(JK|5!iP_N^q z39t8A+>R1Px=zTNGWdj+1>=g?M#cN3qI<$mZ*4ckyfo_k8(2JCEjoDcz^SuwT3b_j- zK(Ym9a5{4p>9e^kT~`M$as%l%KN}-$djH1E{?zooIk$OlWX}C~^%MVU4~aK4{XNqk znSNeXbmI_#ITq$z;LQ2JwV@g8nZd{m&IdMXraE{oW_+uxatUr$r@qteMDw&=C|T$? z?qfzAE6&~qbfPVeuj6ri2PlS>tboa7j4||vn?rQ};*+aWSk2KzvuTX3-8~eJ<4e57 GcmD$YnhvM{ literal 0 HcmV?d00001 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: