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

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">
<th style="width: 40%;">{% trans "Product" %}</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-end">{% trans "Total" %}</th>
<th></th>
@ -72,10 +73,13 @@
<div class="text-muted small">[[ item.sku ]]</div>
</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>
<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 class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
<td class="text-end">
@ -83,7 +87,7 @@
</td>
</tr>
<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." %}
</td>
</tr>
@ -105,10 +109,15 @@
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span>
</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">
<span class="text-muted">{% trans "Discount" %}</span>
<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>
@ -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,

View File

@ -138,7 +138,15 @@
<div>{% trans "Subtotal" %}</div>
<div class="small fw-normal">المجموع الفرعي</div>
</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>
{% if sale.discount > 0 %}
<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">
{% 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="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 %}
<img src="{{ product.image.url }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 80px; object-fit: cover;">
{% else %}
@ -224,6 +224,10 @@
<div class="d-flex justify-content-between mb-2">
<span class="small">{% trans "Subtotal" %}</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 class="d-flex justify-content-between align-items-center mb-2">
<span class="small">{% trans "Discount" %}</span>
@ -443,6 +447,9 @@
<span>Date: <span id="inv-date"></span></span>
<span class="rtl">التاريخ: <span id="inv-date-ar"></span></span>
</div>
<div class="d-flex justify-content-between">
<span>Customer: <span id="inv-customer"></span></span>
</div>
</div>
<table class="invoice-table">
<thead>
@ -457,8 +464,20 @@
</tbody>
</table>
<div class="invoice-total">
<div class="d-flex justify-content-between fw-bold">
<span>TOTAL / المجموع</span>
<div class="d-flex justify-content-between">
<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>
</div>
</div>
@ -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,6 +865,7 @@
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 => {
@ -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,

View File

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