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 "If enabled, sales can be processed even if product stock is 0 or less." %}
+
+
+
{% 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)