adding vat

This commit is contained in:
Flatlogic Bot 2026-02-05 13:09:11 +00:00
parent a123d9bb27
commit 9dfa03d69c
11 changed files with 334 additions and 24 deletions

58
apply_patch.py Normal file
View File

@ -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")

View File

@ -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'),
),
]

View File

@ -147,6 +147,8 @@ class Sale(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales") 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") 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) 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) 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) 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) 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): def save_user_profile(sender, instance, **kwargs):
if not hasattr(instance, 'profile'): if not hasattr(instance, 'profile'):
UserProfile.objects.create(user=instance) UserProfile.objects.create(user=instance)
instance.profile.save() instance.profile.save()

161
core/patch_views_vat.py Normal file
View File

@ -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)

View File

@ -60,6 +60,7 @@
<tr class="small text-uppercase text-muted fw-bold"> <tr class="small text-uppercase text-muted fw-bold">
<th style="width: 40%;">{% trans "Product" %}</th> <th style="width: 40%;">{% trans "Product" %}</th>
<th class="text-center">{% trans "Unit Price" %}</th> <th class="text-center">{% trans "Unit Price" %}</th>
<th class="text-center">{% trans "VAT %" %}</th>
<th class="text-center" style="width: 15%;">{% trans "Quantity" %}</th> <th class="text-center" style="width: 15%;">{% trans "Quantity" %}</th>
<th class="text-end">{% trans "Total" %}</th> <th class="text-end">{% trans "Total" %}</th>
<th></th> <th></th>
@ -72,10 +73,13 @@
<div class="text-muted small">[[ item.sku ]]</div> <div class="text-muted small">[[ item.sku ]]</div>
</td> </td>
<td> <td>
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal"> <input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price">
</td> </td>
<td> <td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal"> <input type="number" step="0.01" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.vat_rate" readonly disabled>
</td>
<td>
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity">
</td> </td>
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td> <td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
<td class="text-end"> <td class="text-end">
@ -83,7 +87,7 @@
</td> </td>
</tr> </tr>
<tr v-if="cart.length === 0"> <tr v-if="cart.length === 0">
<td colspan="5" class="text-center py-5 text-muted"> <td colspan="6" class="text-center py-5 text-muted">
{% trans "Search and add products to this invoice." %} {% trans "Search and add products to this invoice." %}
</td> </td>
</tr> </tr>
@ -105,10 +109,15 @@
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span> <span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span>
</div> </div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">{% trans "VAT" %}</span>
<span class="fw-bold text-danger">[[ currencySymbol ]][[ totalVat.toFixed(decimalPlaces) ]]</span>
</div>
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span class="text-muted">{% trans "Discount" %}</span> <span class="text-muted">{% trans "Discount" %}</span>
<div class="input-group input-group-sm" style="width: 120px;"> <div class="input-group input-group-sm" style="width: 120px;">
<input type="number" class="form-control text-end border-0 border-bottom rounded-0" v-model="discount" @input="calculateTotal"> <input type="number" class="form-control text-end border-0 border-bottom rounded-0" v-model="discount">
</div> </div>
</div> </div>
@ -184,6 +193,7 @@
name_ar: "{{ p.name_ar|escapejs }}", name_ar: "{{ p.name_ar|escapejs }}",
sku: "{{ p.sku|escapejs }}", sku: "{{ p.sku|escapejs }}",
price: {{ p.price|default:0 }}, price: {{ p.price|default:0 }},
vat: {{ p.vat|default:site_settings.tax_rate|default:0 }},
stock: {{ p.stock_quantity|default:0 }} stock: {{ p.stock_quantity|default:0 }}
}, },
{% endfor %} {% endfor %}
@ -208,8 +218,11 @@
subtotal() { subtotal() {
return this.cart.reduce((total, item) => total + (item.price * item.quantity), 0); 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() { grandTotal() {
return Math.max(0, this.subtotal - this.discount); return Math.max(0, this.subtotal + this.totalVat - this.discount);
} }
}, },
methods: { methods: {
@ -235,6 +248,7 @@
name_en: product.name_en, name_en: product.name_en,
sku: product.sku, sku: product.sku,
price: product.price, price: product.price,
vat_rate: product.vat,
quantity: 1 quantity: 1
}); });
} }
@ -264,6 +278,8 @@
price: item.price, price: item.price,
line_total: item.price * item.quantity line_total: item.price * item.quantity
})), })),
subtotal: this.subtotal,
vat_amount: this.totalVat,
total_amount: this.grandTotal, total_amount: this.grandTotal,
discount: this.discount, discount: this.discount,
paid_amount: actualPaidAmount, paid_amount: actualPaidAmount,
@ -300,4 +316,4 @@
}).mount('#saleApp'); }).mount('#saleApp');
</script> </script>
{% endlocalize %} {% endlocalize %}
{% endblock %} {% endblock %}

View File

@ -138,7 +138,15 @@
<div>{% trans "Subtotal" %}</div> <div>{% trans "Subtotal" %}</div>
<div class="small fw-normal">المجموع الفرعي</div> <div class="small fw-normal">المجموع الفرعي</div>
</td> </td>
<td class="text-end pe-4 py-3 fw-bold border-top">{{ settings.currency_symbol }}{{ sale.total_amount|add:sale.discount|floatformat:3 }}</td> <td class="text-end pe-4 py-3 fw-bold border-top">{{ settings.currency_symbol }}{{ sale.subtotal|floatformat:3 }}</td>
</tr>
<tr>
<td colspan="2" class="border-0"></td>
<td class="text-center py-2 fw-bold text-muted">
<div>{% trans "VAT" %}</div>
<div class="small fw-normal">الضريبة</div>
</td>
<td class="text-end pe-4 py-2 fw-bold text-muted">{{ settings.currency_symbol }}{{ sale.vat_amount|floatformat:3 }}</td>
</tr> </tr>
{% if sale.discount > 0 %} {% if sale.discount > 0 %}
<tr class="text-muted"> <tr class="text-muted">

View File

@ -146,7 +146,7 @@
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-2" id="productGrid"> <div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-2" id="productGrid">
{% for product in products %} {% for product in products %}
<div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}"> <div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}">
<div class="card h-100 shadow-sm product-card p-1" onclick="addToCart({{ product.id|unlocalize }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price|unlocalize }})"> <div class="card h-100 shadow-sm product-card p-1" onclick="addToCart({{ product.id|unlocalize }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price|unlocalize }}, {{ product.vat|default:site_settings.tax_rate|default:0|unlocalize }})">
{% if product.image %} {% if product.image %}
<img src="{{ product.image.url }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 80px; object-fit: cover;"> <img src="{{ product.image.url }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 80px; object-fit: cover;">
{% else %} {% else %}
@ -224,6 +224,10 @@
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span class="small">{% trans "Subtotal" %}</span> <span class="small">{% trans "Subtotal" %}</span>
<span id="subtotalAmount" class="small">{{ site_settings.currency_symbol }}0.000</span> <span id="subtotalAmount" class="small">{{ site_settings.currency_symbol }}0.000</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="small">{% trans "VAT" %}</span>
<span id="taxAmount" class="small">{{ site_settings.currency_symbol }}0.000</span>
</div> </div>
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<span class="small">{% trans "Discount" %}</span> <span class="small">{% trans "Discount" %}</span>
@ -443,6 +447,9 @@
<span>Date: <span id="inv-date"></span></span> <span>Date: <span id="inv-date"></span></span>
<span class="rtl">التاريخ: <span id="inv-date-ar"></span></span> <span class="rtl">التاريخ: <span id="inv-date-ar"></span></span>
</div> </div>
<div class="d-flex justify-content-between">
<span>Customer: <span id="inv-customer"></span></span>
</div>
</div> </div>
<table class="invoice-table"> <table class="invoice-table">
<thead> <thead>
@ -457,8 +464,20 @@
</tbody> </tbody>
</table> </table>
<div class="invoice-total"> <div class="invoice-total">
<div class="d-flex justify-content-between fw-bold"> <div class="d-flex justify-content-between">
<span>TOTAL / المجموع</span> <span>Subtotal / المجموع الفرعي</span>
<span id="inv-subtotal"></span>
</div>
<div class="d-flex justify-content-between">
<span>VAT / الضريبة</span>
<span id="inv-vat"></span>
</div>
<div class="d-flex justify-content-between">
<span>Discount / الخصم</span>
<span id="inv-discount"></span>
</div>
<div class="d-flex justify-content-between fw-bold" style="border-top: 1px solid #000; margin-top: 2px; padding-top: 2px;">
<span>TOTAL / المجموع النهائي</span>
<span id="inv-total"></span> <span id="inv-total"></span>
</div> </div>
</div> </div>
@ -514,7 +533,7 @@
document.body.classList.toggle('cart-open'); 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); const existing = cart.find(item => item.id === id);
if (existing) { if (existing) {
existing.quantity += 1; existing.quantity += 1;
@ -525,6 +544,7 @@
name_en: nameEn, name_en: nameEn,
name_ar: nameAr, name_ar: nameAr,
price, price,
vat_rate: vatRate,
quantity: 1, quantity: 1,
line_total: price line_total: price
}); });
@ -556,10 +576,12 @@
function updateTotals() { function updateTotals() {
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); 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 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('subtotalAmount').innerText = `${currency} ${formatAmount(subtotal)}`;
document.getElementById('taxAmount').innerText = `${currency} ${formatAmount(totalVat)}`;
document.getElementById('totalAmount').innerText = `${currency} ${formatAmount(total)}`; document.getElementById('totalAmount').innerText = `${currency} ${formatAmount(total)}`;
} }
@ -612,8 +634,9 @@
if (cart.length === 0) return; if (cart.length === 0) return;
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); 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 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('modalTotalAmount').innerText = `${currency} ${formatAmount(totalAmount)}`;
document.getElementById('cashReceivedInput').value = ''; document.getElementById('cashReceivedInput').value = '';
@ -686,8 +709,9 @@
// Update Total Payable // Update Total Payable
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); 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 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)}`; document.getElementById('modalTotalAmount').innerText = `${currency} ${formatAmount(totalAmount)}`;
calculateBalance(); calculateBalance();
@ -714,11 +738,12 @@
function setExactAmount() { function setExactAmount() {
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); 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 discount = parseFloat(document.getElementById('discountInput').value) || 0;
const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0; const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0;
const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 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); document.getElementById('cashReceivedInput').value = totalAmount.toFixed(decimalPlaces);
calculateBalance(); calculateBalance();
@ -731,11 +756,12 @@
function calculateBalance() { function calculateBalance() {
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); 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 discount = parseFloat(document.getElementById('discountInput').value) || 0;
const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0; const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0;
const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 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 received = parseFloat(document.getElementById('cashReceivedInput').value) || 0;
const balance = Math.max(0, received - totalAmount); const balance = Math.max(0, received - totalAmount);
@ -751,16 +777,19 @@
confirmBtn.innerText = '{% trans "Processing..." %}'; confirmBtn.innerText = '{% trans "Processing..." %}';
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); 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 discount = parseFloat(document.getElementById('discountInput').value) || 0;
const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0; const loyaltyRedeem = parseFloat(document.getElementById('loyaltyRedeemInput').value) || 0;
const loyaltyDiscount = customerLoyalty ? loyaltyRedeem * customerLoyalty.currency_per_point : 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 = { const data = {
customer_id: document.getElementById('customerSelect').value, customer_id: document.getElementById('customerSelect').value,
payment_method_id: currentPaymentType === 'cash' ? selectedPaymentMethodId : null, payment_method_id: currentPaymentType === 'cash' ? selectedPaymentMethodId : null,
items: cart, items: cart,
subtotal: subtotal,
vat_amount: totalVat,
total_amount: totalAmount, total_amount: totalAmount,
paid_amount: currentPaymentType === 'cash' ? totalAmount : 0, paid_amount: currentPaymentType === 'cash' ? totalAmount : 0,
discount: discount, discount: discount,
@ -836,7 +865,8 @@
document.getElementById('inv-id-ar').innerText = data.sale.id; document.getElementById('inv-id-ar').innerText = data.sale.id;
document.getElementById('inv-date').innerText = data.sale.created_at; document.getElementById('inv-date').innerText = data.sale.created_at;
document.getElementById('inv-date-ar').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 = ''; let itemsHtml = '';
data.sale.items.forEach(item => { data.sale.items.forEach(item => {
itemsHtml += ` itemsHtml += `
@ -851,6 +881,10 @@
`; `;
}); });
document.getElementById('inv-items').innerHTML = itemsHtml; 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); document.getElementById('inv-total').innerText = data.business.currency + ' ' + formatAmount(data.sale.total);
} }
@ -906,8 +940,9 @@
if (cart.length === 0) return; if (cart.length === 0) return;
const subtotal = cart.reduce((acc, item) => acc + item.line_total, 0); 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 discount = parseFloat(document.getElementById('discountInput').value) || 0;
const totalAmount = Math.max(0, subtotal - discount); const totalAmount = Math.max(0, subtotal + totalVat - discount);
const data = { const data = {
customer_id: document.getElementById('customerSelect').value, customer_id: document.getElementById('customerSelect').value,

View File

@ -426,7 +426,12 @@ def create_sale_api(request):
customer_id = data.get('customer_id') customer_id = data.get('customer_id')
invoice_number = data.get('invoice_number', '') invoice_number = data.get('invoice_number', '')
items = data.get('items', []) 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) total_amount = data.get('total_amount', 0)
paid_amount = data.get('paid_amount', 0) paid_amount = data.get('paid_amount', 0)
discount = data.get('discount', 0) discount = data.get('discount', 0)
payment_type = data.get('payment_type', 'cash') payment_type = data.get('payment_type', 'cash')
@ -456,6 +461,8 @@ def create_sale_api(request):
sale = Sale.objects.create( sale = Sale.objects.create(
customer=customer, customer=customer,
invoice_number=invoice_number, invoice_number=invoice_number,
subtotal=subtotal,
vat_amount=vat_amount,
total_amount=total_amount, total_amount=total_amount,
paid_amount=paid_amount, paid_amount=paid_amount,
balance_due=float(total_amount) - float(paid_amount), balance_due=float(total_amount) - float(paid_amount),
@ -552,9 +559,13 @@ def create_sale_api(request):
'id': sale.id, 'id': sale.id,
'invoice_number': sale.invoice_number, 'invoice_number': sale.invoice_number,
'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"), '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), 'total': float(sale.total_amount),
'discount': float(sale.discount),
'paid': float(sale.paid_amount), 'paid': float(sale.paid_amount),
'balance': float(sale.balance_due), 'balance': float(sale.balance_due),
'customer_name': sale.customer.name if sale.customer else 'Guest',
'items': [ 'items': [
{ {
'name_en': si.product.name_en, 'name_en': si.product.name_en,
@ -568,10 +579,6 @@ def create_sale_api(request):
}) })
except Exception as e: except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400) 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): def send_invoice_whatsapp(request):
if request.method == 'POST': if request.method == 'POST':
try: try: