diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc
index cab57bd..3b99c65 100644
Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ
diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc
index ff8faf4..33b7d83 100644
Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ
diff --git a/core/models.py b/core/models.py
index be90b4d..eac88e7 100644
--- a/core/models.py
+++ b/core/models.py
@@ -396,6 +396,9 @@ class SystemSetting(models.Model):
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True)
+ # Financial Settings
+ allow_zero_stock_sales = models.BooleanField(_("Allow selling items with 0 stock"), default=False)
+
# Loyalty Settings
loyalty_enabled = models.BooleanField(_("Enable Loyalty System"), default=False)
points_per_currency = models.DecimalField(_("Points Earned per Currency Unit"), max_digits=10, decimal_places=2, default=1.0)
diff --git a/core/templates/core/settings.html b/core/templates/core/settings.html
index 5c16a86..19810d1 100644
--- a/core/templates/core/settings.html
+++ b/core/templates/core/settings.html
@@ -120,6 +120,14 @@
{% trans "For price display" %}
+
+
{% trans "Loyalty Configuration" %}
diff --git a/core/views.py b/core/views.py
index fdaf73a..85890aa 100644
--- a/core/views.py
+++ b/core/views.py
@@ -467,166 +467,15 @@ def create_sale_api(request):
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)
-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()
-
+ # Check for stock availability if overselling is not allowed
+ if not settings.allow_zero_stock_sales:
+ for item in items:
+ try:
+ product = Product.objects.get(id=item["id"])
+ if product.stock_quantity < float(item["quantity"]):
+ return JsonResponse({"success": False, "error": _("Insufficient stock for product: ") + product.name_en}, status=400)
+ except Product.DoesNotExist:
+ pass
loyalty_discount = 0
if settings.loyalty_enabled and customer and points_to_redeem > 0:
if customer.loyalty_points >= points_to_redeem:
@@ -909,6 +758,14 @@ def convert_quotation_to_invoice(request, pk):
if quotation.status == 'converted':
messages.warning(request, _("This quotation has already been converted to an invoice."))
return redirect('invoices')
+
+ # Check stock before converting
+ settings = SystemSetting.objects.first() or SystemSetting.objects.create()
+ if not settings.allow_zero_stock_sales:
+ for item in quotation.items.all():
+ if item.product.stock_quantity < item.quantity:
+ messages.error(request, _("Insufficient stock for product: ") + item.product.name_en)
+ return redirect('quotation_detail', pk=pk)
# Create Sale from Quotation
sale = Sale.objects.create(
@@ -1173,6 +1030,7 @@ def settings_view(request):
settings.decimal_places = request.POST.get("decimal_places", 3)
settings.vat_number = request.POST.get("vat_number", "")
settings.registration_number = request.POST.get("registration_number", "")
+ settings.allow_zero_stock_sales = request.POST.get("allow_zero_stock_sales") == "on"
settings.loyalty_enabled = request.POST.get("loyalty_enabled") == "on"
settings.points_per_currency = request.POST.get("points_per_currency", 1.0)