adding vat
This commit is contained in:
parent
a123d9bb27
commit
9dfa03d69c
58
apply_patch.py
Normal file
58
apply_patch.py
Normal 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")
|
||||||
Binary file not shown.
Binary file not shown.
23
core/migrations/0025_sale_subtotal_sale_vat_amount.py
Normal file
23
core/migrations/0025_sale_subtotal_sale_vat_amount.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -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)
|
||||||
|
|||||||
161
core/patch_views_vat.py
Normal file
161
core/patch_views_vat.py
Normal 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)
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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,6 +865,7 @@
|
|||||||
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 => {
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user