Autosave: 20260206-152701
This commit is contained in:
parent
42e2393347
commit
9d3b9739fa
Binary file not shown.
Binary file not shown.
@ -396,6 +396,9 @@ class SystemSetting(models.Model):
|
|||||||
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
|
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
|
||||||
registration_number = models.CharField(_("Registration 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 Settings
|
||||||
loyalty_enabled = models.BooleanField(_("Enable Loyalty System"), default=False)
|
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)
|
points_per_currency = models.DecimalField(_("Points Earned per Currency Unit"), max_digits=10, decimal_places=2, default=1.0)
|
||||||
|
|||||||
@ -120,6 +120,14 @@
|
|||||||
<div class="form-text">{% trans "For price display" %}</div>
|
<div class="form-text">{% trans "For price display" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 mt-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" name="allow_zero_stock_sales" {% if settings.allow_zero_stock_sales %}checked{% endif %}>
|
||||||
|
<label class="form-check-label fw-bold">{% trans "Allow selling items with 0 stock" %}</label>
|
||||||
|
<div class="form-text">{% trans "If enabled, sales can be processed even if product stock is 0 or less." %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
<h5 class="fw-bold mb-3">{% trans "Loyalty Configuration" %}</h5>
|
<h5 class="fw-bold mb-3">{% trans "Loyalty Configuration" %}</h5>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
178
core/views.py
178
core/views.py
@ -467,166 +467,15 @@ def create_sale_api(request):
|
|||||||
if not settings:
|
if not settings:
|
||||||
settings = SystemSetting.objects.create()
|
settings = SystemSetting.objects.create()
|
||||||
|
|
||||||
loyalty_discount = 0
|
# Check for stock availability if overselling is not allowed
|
||||||
if settings.loyalty_enabled and customer and points_to_redeem > 0:
|
if not settings.allow_zero_stock_sales:
|
||||||
if customer.loyalty_points >= points_to_redeem:
|
for item in items:
|
||||||
loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point)
|
try:
|
||||||
|
product = Product.objects.get(id=item["id"])
|
||||||
sale = Sale.objects.create(
|
if product.stock_quantity < float(item["quantity"]):
|
||||||
customer=customer,
|
return JsonResponse({"success": False, "error": _("Insufficient stock for product: ") + product.name_en}, status=400)
|
||||||
invoice_number=invoice_number,
|
except Product.DoesNotExist:
|
||||||
subtotal=subtotal,
|
pass
|
||||||
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()
|
|
||||||
|
|
||||||
loyalty_discount = 0
|
loyalty_discount = 0
|
||||||
if settings.loyalty_enabled and customer and points_to_redeem > 0:
|
if settings.loyalty_enabled and customer and points_to_redeem > 0:
|
||||||
if customer.loyalty_points >= points_to_redeem:
|
if customer.loyalty_points >= points_to_redeem:
|
||||||
@ -909,6 +758,14 @@ def convert_quotation_to_invoice(request, pk):
|
|||||||
if quotation.status == 'converted':
|
if quotation.status == 'converted':
|
||||||
messages.warning(request, _("This quotation has already been converted to an invoice."))
|
messages.warning(request, _("This quotation has already been converted to an invoice."))
|
||||||
return redirect('invoices')
|
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
|
# Create Sale from Quotation
|
||||||
sale = Sale.objects.create(
|
sale = Sale.objects.create(
|
||||||
@ -1173,6 +1030,7 @@ def settings_view(request):
|
|||||||
settings.decimal_places = request.POST.get("decimal_places", 3)
|
settings.decimal_places = request.POST.get("decimal_places", 3)
|
||||||
settings.vat_number = request.POST.get("vat_number", "")
|
settings.vat_number = request.POST.get("vat_number", "")
|
||||||
settings.registration_number = request.POST.get("registration_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.loyalty_enabled = request.POST.get("loyalty_enabled") == "on"
|
||||||
settings.points_per_currency = request.POST.get("points_per_currency", 1.0)
|
settings.points_per_currency = request.POST.get("points_per_currency", 1.0)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user