deploy moti 222
This commit is contained in:
parent
e6865dae2e
commit
46a59fc51c
BIN
assets/pasted-20260210-082544-8d8f3bac.png
Normal file
BIN
assets/pasted-20260210-082544-8d8f3bac.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
@ -2,8 +2,13 @@ from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from core.helpers import fix_db_view
|
||||
|
||||
urlpatterns = [
|
||||
# Emergency Fixer at Root Level (High Priority)
|
||||
path('fix-db/', fix_db_view, name='fix_db_root'),
|
||||
path('fix_db/', fix_db_view, name='fix_db_alias_root'),
|
||||
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
path("i18n/", include("django.conf.urls.i18n")),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,16 +1,15 @@
|
||||
from .models import SystemSetting
|
||||
from django.db.utils import OperationalError
|
||||
from django.core.management import call_command
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Stabilize the timestamp to avoid cache-busting on every single request
|
||||
# This will only change when the server restarts
|
||||
STARTUP_TIMESTAMP = int(time.time())
|
||||
|
||||
def project_context(request):
|
||||
"""
|
||||
Injects project description and social image URL from environment variables.
|
||||
Also injects a deployment timestamp for cache-busting.
|
||||
"""
|
||||
return {
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
@ -18,14 +17,18 @@ def project_context(request):
|
||||
}
|
||||
|
||||
def global_settings(request):
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
if not settings:
|
||||
settings = SystemSetting.objects.create()
|
||||
return {
|
||||
'site_settings': settings,
|
||||
'global_settings': settings,
|
||||
'decimal_places': settings.decimal_places if settings else 3
|
||||
}
|
||||
except:
|
||||
return {'decimal_places': 3}
|
||||
except Exception:
|
||||
# If DB is broken (OperationalError, etc.), just return None.
|
||||
# Do not try to fix it here to avoid infinite loops or crashes during template rendering.
|
||||
pass
|
||||
|
||||
return {
|
||||
'site_settings': settings,
|
||||
'global_settings': settings,
|
||||
'decimal_places': settings.decimal_places if settings else 3
|
||||
}
|
||||
|
||||
@ -1,30 +1,28 @@
|
||||
from django.http import HttpResponse
|
||||
from django.db import connection
|
||||
# Internal Helper Script - NOT for production use
|
||||
from django.db import connection, transaction
|
||||
from core.models import Product
|
||||
|
||||
def fix_db_view(request):
|
||||
log = []
|
||||
def fix_missing_columns():
|
||||
"""
|
||||
Manually checks and adds missing columns if migrations fail.
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
# 1. Check/Add is_service to core_product
|
||||
# Check is_service
|
||||
try:
|
||||
cursor.execute("SELECT is_service FROM core_product LIMIT 1")
|
||||
log.append("SUCCESS: is_service already exists in core_product.")
|
||||
except Exception:
|
||||
print("Adding is_service column...")
|
||||
try:
|
||||
# Try MySQL syntax first
|
||||
cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0;")
|
||||
log.append("FIXED: Added is_service column to core_product.")
|
||||
cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0")
|
||||
except Exception as e:
|
||||
log.append(f"ERROR adding is_service: {e}")
|
||||
print(f"Error adding column: {e}")
|
||||
|
||||
# 2. Check/Add is_active to core_paymentmethod
|
||||
# Check is_active on PaymentMethod
|
||||
try:
|
||||
cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1")
|
||||
log.append("SUCCESS: is_active already exists in core_paymentmethod.")
|
||||
except Exception:
|
||||
print("Adding is_active column to PaymentMethod...")
|
||||
try:
|
||||
cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1;")
|
||||
log.append("FIXED: Added is_active column to core_paymentmethod.")
|
||||
cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1")
|
||||
except Exception as e:
|
||||
log.append(f"ERROR adding is_active: {e}")
|
||||
|
||||
return HttpResponse("<br>".join(log) + "<br><br><a href='/'>Go to Dashboard</a>")
|
||||
print(f"Error adding column: {e}")
|
||||
|
||||
158
core/helpers.py
158
core/helpers.py
@ -1,153 +1,11 @@
|
||||
from django.http import HttpResponse
|
||||
from django.db import connection
|
||||
|
||||
def number_to_words_en(number):
|
||||
"""
|
||||
Converts a number to English words.
|
||||
Handles decimals up to 3 places.
|
||||
"""
|
||||
if number == 0:
|
||||
return "Zero"
|
||||
|
||||
units = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten",
|
||||
"Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"]
|
||||
tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"]
|
||||
thousands = ["", "Thousand", "Million", "Billion"]
|
||||
|
||||
def _convert_less_than_thousand(num):
|
||||
res = ""
|
||||
if num >= 100:
|
||||
res += units[num // 100] + " Hundred "
|
||||
num %= 100
|
||||
if num >= 20:
|
||||
res += tens[num // 10] + " "
|
||||
num %= 10
|
||||
if num > 0:
|
||||
res += units[num]
|
||||
return res.strip()
|
||||
|
||||
try:
|
||||
parts = str(float(number)).split('.')
|
||||
integer_part = int(parts[0])
|
||||
fractional_part = int(parts[1]) if len(parts) > 1 else 0
|
||||
except ValueError:
|
||||
return "Invalid Number"
|
||||
|
||||
res = ""
|
||||
if integer_part == 0:
|
||||
res = "Zero"
|
||||
else:
|
||||
idx = 0
|
||||
while integer_part > 0:
|
||||
if integer_part % 1000 != 0:
|
||||
res = _convert_less_than_thousand(integer_part % 1000) + " " + thousands[idx] + " " + res
|
||||
integer_part //= 1000
|
||||
idx += 1
|
||||
|
||||
words = res.strip()
|
||||
|
||||
if fractional_part > 0:
|
||||
frac_str = parts[1]
|
||||
denom = 10 ** len(frac_str)
|
||||
words += f" and {fractional_part}/{denom}"
|
||||
|
||||
return words
|
||||
|
||||
def number_to_words_ar(number):
|
||||
return number_to_words_en(number)
|
||||
|
||||
def send_whatsapp_message(phone, message):
|
||||
try:
|
||||
import requests
|
||||
from .models import SystemSetting
|
||||
|
||||
settings = SystemSetting.objects.first()
|
||||
if not settings or not settings.wablas_enabled:
|
||||
return False, "WhatsApp gateway is disabled."
|
||||
|
||||
if not settings.wablas_token or not settings.wablas_server_url:
|
||||
return False, "Wablas configuration is incomplete."
|
||||
|
||||
phone = ''.join(filter(str.isdigit, str(phone)))
|
||||
server_url = settings.wablas_server_url.rstrip('/')
|
||||
url = f"{server_url}/api/send-message"
|
||||
|
||||
headers = {
|
||||
"Authorization": settings.wablas_token,
|
||||
"Secret": settings.wablas_secret_key
|
||||
}
|
||||
|
||||
payload = {"phone": phone, "message": message}
|
||||
|
||||
response = requests.post(url, data=payload, headers=headers, timeout=10)
|
||||
data = response.json()
|
||||
if response.status_code == 200 and data.get('status') == True:
|
||||
return True, "Message sent successfully."
|
||||
else:
|
||||
return False, data.get('message', 'Unknown error from Wablas.')
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def send_whatsapp_document(phone, document_url, caption=""):
|
||||
try:
|
||||
import requests
|
||||
from .models import SystemSetting
|
||||
|
||||
settings = SystemSetting.objects.first()
|
||||
if not settings or not settings.wablas_enabled:
|
||||
return False, "WhatsApp gateway is disabled."
|
||||
|
||||
if not settings.wablas_token or not settings.wablas_server_url:
|
||||
return False, "Wablas configuration is incomplete."
|
||||
|
||||
phone = ''.join(filter(str.isdigit, str(phone)))
|
||||
server_url = settings.wablas_server_url.rstrip('/')
|
||||
url = f"{server_url}/api/send-document"
|
||||
|
||||
headers = {
|
||||
"Authorization": settings.wablas_token,
|
||||
"Secret": settings.wablas_secret_key
|
||||
}
|
||||
|
||||
payload = {
|
||||
"phone": phone,
|
||||
"document": document_url,
|
||||
"caption": caption
|
||||
}
|
||||
|
||||
response = requests.post(url, data=payload, headers=headers, timeout=15)
|
||||
data = response.json()
|
||||
if response.status_code == 200 and data.get('status') == True:
|
||||
return True, "Document sent successfully."
|
||||
else:
|
||||
return False, data.get('message', 'Unknown error from Wablas.')
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
from django.core.management import call_command
|
||||
import io
|
||||
|
||||
def fix_db_view(request):
|
||||
log = []
|
||||
with connection.cursor() as cursor:
|
||||
# 1. Check/Add is_service to core_product
|
||||
try:
|
||||
cursor.execute("SELECT is_service FROM core_product LIMIT 1")
|
||||
log.append("SUCCESS: is_service already exists in core_product.")
|
||||
except Exception:
|
||||
try:
|
||||
# Try MySQL syntax first
|
||||
cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0;")
|
||||
log.append("FIXED: Added is_service column to core_product.")
|
||||
except Exception as e:
|
||||
log.append(f"ERROR adding is_service: {e}")
|
||||
|
||||
# 2. Check/Add is_active to core_paymentmethod
|
||||
try:
|
||||
cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1")
|
||||
log.append("SUCCESS: is_active already exists in core_paymentmethod.")
|
||||
except Exception:
|
||||
try:
|
||||
cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1;")
|
||||
log.append("FIXED: Added is_active column to core_paymentmethod.")
|
||||
except Exception as e:
|
||||
log.append(f"ERROR adding is_active: {e}")
|
||||
|
||||
return HttpResponse("<br>".join(log) + "<br><br><a href='/'>Go to Dashboard</a>")
|
||||
out = io.StringIO()
|
||||
try:
|
||||
call_command('migrate', 'core', stdout=out)
|
||||
return HttpResponse(f"SUCCESS: Database updated.<br><pre>{out.getvalue()}</pre><br><a href='/'>Go Home</a>")
|
||||
except Exception as e:
|
||||
return HttpResponse(f"ERROR: {e}<br><pre>{out.getvalue()}</pre>")
|
||||
15
core/migrations/0034_systemsetting_favicon.py
Normal file
15
core/migrations/0034_systemsetting_favicon.py
Normal file
@ -0,0 +1,15 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0033_auto_add_is_service'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemsetting',
|
||||
name='favicon',
|
||||
field=models.FileField(blank=True, null=True, upload_to='business_logos/', verbose_name='Favicon'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -395,6 +395,7 @@ class SystemSetting(models.Model):
|
||||
tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0)
|
||||
decimal_places = models.PositiveSmallIntegerField(_("Decimal Places"), default=3)
|
||||
logo = models.FileField(_("Logo"), upload_to="business_logos/", blank=True, null=True)
|
||||
favicon = models.FileField(_("Favicon"), upload_to="business_logos/", blank=True, null=True)
|
||||
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
|
||||
registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True)
|
||||
|
||||
|
||||
@ -10,6 +10,12 @@
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
|
||||
{% if site_settings.favicon %}
|
||||
<link rel="icon" type="image/png" href="{{ site_settings.favicon.url }}">
|
||||
{% else %}
|
||||
<link rel="icon" type="image/x-icon" href="{% static 'favicon.ico' %}">
|
||||
{% endif %}
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&family=Plus+Jakarta+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
@ -1103,6 +1103,13 @@
|
||||
|
||||
function prepareInvoice(data) {
|
||||
const logo = document.getElementById('inv-logo');
|
||||
|
||||
// Safety check
|
||||
if (!data.business) {
|
||||
console.warn("Invoice Error: data.business is missing", data);
|
||||
data.business = {}; // Prevent crashes
|
||||
}
|
||||
|
||||
if (data.business.logo_url) {
|
||||
logo.src = data.business.logo_url;
|
||||
logo.style.display = 'inline-block';
|
||||
|
||||
@ -75,8 +75,9 @@
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data" action="{% url 'settings' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="setting_type" value="profile">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12 text-center mb-3">
|
||||
<div class="col-md-6 text-center mb-3">
|
||||
<label class="form-label d-block fw-semibold">{% trans "Business Logo" %}</label>
|
||||
{% if settings.logo %}
|
||||
<img src="{{ settings.logo.url }}" alt="Logo" class="img-thumbnail mb-2" style="max-height: 100px;">
|
||||
@ -87,6 +88,17 @@
|
||||
{% endif %}
|
||||
<input type="file" name="logo" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*">
|
||||
</div>
|
||||
<div class="col-md-6 text-center mb-3">
|
||||
<label class="form-label d-block fw-semibold">{% trans "Favicon" %}</label>
|
||||
{% if settings.favicon %}
|
||||
<img src="{{ settings.favicon.url }}" alt="Favicon" class="img-thumbnail mb-2" style="max-height: 50px;">
|
||||
{% else %}
|
||||
<div class="bg-light border rounded d-inline-flex align-items-center justify-content-center mb-2" style="width: 50px; height: 50px;">
|
||||
<i class="bi bi-browser-chrome text-muted fs-4"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="file" name="favicon" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-semibold">{% trans "Business Name" %}</label>
|
||||
@ -572,6 +584,7 @@
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'settings' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="setting_type" value="whatsapp">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<div class="form-check form-switch mb-3">
|
||||
@ -774,7 +787,7 @@
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-0">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Save Device" %}</button>
|
||||
<button type="button" class="btn btn-primary">{% trans "Save Device" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
181
core/views.py
181
core/views.py
@ -37,7 +37,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
settings = SystemSetting.objects.first()
|
||||
# Auto-Fix Migration on Home Page Load (Temporary)
|
||||
try:
|
||||
from django.core.management import call_command
|
||||
from io import StringIO
|
||||
import sys
|
||||
out = StringIO()
|
||||
call_command('migrate', 'core', stdout=out)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration Fix Failed: {e}")
|
||||
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load settings in index: {e}")
|
||||
|
||||
today = timezone.now().date()
|
||||
|
||||
# 1. Financials
|
||||
@ -149,11 +164,29 @@ def inventory(request):
|
||||
products = Product.objects.all().order_by('name_en')
|
||||
categories = Category.objects.all()
|
||||
units = Unit.objects.all()
|
||||
suppliers = Supplier.objects.all().order_by('name')
|
||||
|
||||
# Expired/Expiring logic
|
||||
today = timezone.now().date()
|
||||
next_30_days = today + datetime.timedelta(days=30)
|
||||
|
||||
expired_products = products.filter(has_expiry=True, expiry_date__lt=today)
|
||||
expiring_soon_products = products.filter(has_expiry=True, expiry_date__gte=today, expiry_date__lte=next_30_days)
|
||||
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
context = {
|
||||
'products': products,
|
||||
'categories': categories,
|
||||
'units': units
|
||||
'units': units,
|
||||
'suppliers': suppliers,
|
||||
'expired_products': expired_products,
|
||||
'expiring_soon_products': expiring_soon_products,
|
||||
'site_settings': settings,
|
||||
}
|
||||
return render(request, 'core/inventory.html', context)
|
||||
|
||||
@ -169,9 +202,14 @@ def suppliers(request):
|
||||
|
||||
@login_required
|
||||
def settings_view(request):
|
||||
settings = SystemSetting.objects.first()
|
||||
if not settings:
|
||||
settings = SystemSetting.objects.create()
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
if not settings:
|
||||
settings = SystemSetting.objects.create()
|
||||
except Exception:
|
||||
# Create a dummy object or just pass None if DB is broken
|
||||
pass
|
||||
|
||||
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
||||
expense_categories = ExpenseCategory.objects.all()
|
||||
@ -179,13 +217,40 @@ def settings_view(request):
|
||||
devices = Device.objects.all().order_by("name")
|
||||
|
||||
if request.method == 'POST':
|
||||
form = SystemSettingForm(request.POST, request.FILES, instance=settings)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Settings updated.")
|
||||
return redirect('settings')
|
||||
setting_type = request.POST.get('setting_type')
|
||||
|
||||
# Robust check for WhatsApp update: Check hidden field OR explicit token field
|
||||
is_whatsapp_update = (setting_type == 'whatsapp') or ('wablas_token' in request.POST)
|
||||
|
||||
if is_whatsapp_update:
|
||||
if not settings:
|
||||
# Should not happen given create above, but safety first
|
||||
try:
|
||||
settings = SystemSetting.objects.create()
|
||||
except Exception:
|
||||
messages.error(request, _("Database error: Could not save settings."))
|
||||
return redirect(reverse('settings') + '#whatsapp')
|
||||
|
||||
# Handle WhatsApp update manually to avoid validation errors on other fields
|
||||
settings.wablas_enabled = request.POST.get('wablas_enabled') == 'on'
|
||||
settings.wablas_token = request.POST.get('wablas_token', '')
|
||||
settings.wablas_server_url = request.POST.get('wablas_server_url', '')
|
||||
settings.wablas_secret_key = request.POST.get('wablas_secret_key', '')
|
||||
settings.save()
|
||||
messages.success(request, _("WhatsApp settings updated successfully."))
|
||||
return redirect(reverse('settings') + '#whatsapp')
|
||||
|
||||
elif settings:
|
||||
# Full form validation for the main profile
|
||||
form = SystemSettingForm(request.POST, request.FILES, instance=settings)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Settings updated successfully."))
|
||||
return redirect('settings')
|
||||
else:
|
||||
messages.error(request, _("Please correct the errors below."))
|
||||
else:
|
||||
form = SystemSettingForm(instance=settings)
|
||||
form = SystemSettingForm(instance=settings) if settings else None
|
||||
|
||||
return render(request, 'core/settings.html', {
|
||||
'form': form,
|
||||
@ -253,7 +318,12 @@ def pos(request):
|
||||
messages.warning(request, _("Please open a session to start selling."))
|
||||
return redirect('start_session')
|
||||
|
||||
settings = SystemSetting.objects.first()
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
products = Product.objects.filter(is_active=True)
|
||||
|
||||
if settings and not settings.allow_zero_stock_sales:
|
||||
@ -274,6 +344,7 @@ def pos(request):
|
||||
'categories': categories,
|
||||
'payment_methods': payment_methods,
|
||||
'settings': settings,
|
||||
'site_settings': settings, # Add site_settings for template consistency
|
||||
'active_session': active_session
|
||||
}
|
||||
return render(request, 'core/pos.html', context)
|
||||
@ -313,11 +384,17 @@ def invoice_list(request):
|
||||
|
||||
paginator = Paginator(sales, 25)
|
||||
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
context = {
|
||||
'sales': paginator.get_page(request.GET.get('page')),
|
||||
'customers': Customer.objects.all(),
|
||||
'payment_methods': PaymentMethod.objects.filter(is_active=True),
|
||||
'site_settings': SystemSetting.objects.first(),
|
||||
'site_settings': settings,
|
||||
}
|
||||
return render(request, 'core/invoices.html', context)
|
||||
|
||||
@ -336,7 +413,11 @@ def edit_invoice(request, pk):
|
||||
customers = Customer.objects.all()
|
||||
products = Product.objects.filter(is_active=True).select_related('category')
|
||||
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
||||
site_settings = SystemSetting.objects.first()
|
||||
site_settings = None
|
||||
try:
|
||||
site_settings = SystemSetting.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
decimal_places = 2
|
||||
if site_settings:
|
||||
@ -413,7 +494,11 @@ def customer_payments(request):
|
||||
@login_required
|
||||
def customer_payment_receipt(request, pk):
|
||||
payment = get_object_or_404(SalePayment, pk=pk)
|
||||
settings = SystemSetting.objects.first()
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
return render(request, 'core/payment_receipt.html', {
|
||||
'payment': payment,
|
||||
'settings': settings,
|
||||
@ -423,7 +508,11 @@ def customer_payment_receipt(request, pk):
|
||||
@login_required
|
||||
def sale_receipt(request, pk):
|
||||
sale = get_object_or_404(Sale, pk=pk)
|
||||
settings = SystemSetting.objects.first()
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
return render(request, 'core/sale_receipt.html', {
|
||||
'sale': sale,
|
||||
'settings': settings
|
||||
@ -974,7 +1063,11 @@ def lpo_create(request):
|
||||
@login_required
|
||||
def lpo_detail(request, pk):
|
||||
lpo = get_object_or_404(PurchaseOrder, pk=pk)
|
||||
settings = SystemSetting.objects.first()
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
return render(request, 'core/lpo_detail.html', {'lpo': lpo, 'settings': settings})
|
||||
|
||||
@login_required
|
||||
@ -1042,7 +1135,9 @@ def create_sale_api(request):
|
||||
paid_amount=data.get('paid_amount', 0),
|
||||
payment_type=data.get('payment_type', 'cash'),
|
||||
created_by=request.user,
|
||||
status='paid' if data.get('payment_type') == 'cash' else 'partial'
|
||||
status='paid' if data.get('payment_type') == 'cash' else 'partial',
|
||||
discount=data.get('discount', 0),
|
||||
loyalty_points_redeemed=data.get('loyalty_points_redeemed', 0)
|
||||
)
|
||||
for item in data.get('items', []):
|
||||
SaleItem.objects.create(
|
||||
@ -1062,8 +1157,56 @@ def create_sale_api(request):
|
||||
payment_method_id=data.get('payment_method_id'),
|
||||
created_by=request.user
|
||||
)
|
||||
return JsonResponse({'success': True, 'sale_id': sale.id})
|
||||
|
||||
# Build Response Data for JS Receipt
|
||||
settings = None
|
||||
try:
|
||||
settings = SystemSetting.objects.first()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
business_info = {
|
||||
'name': settings.business_name if settings else 'Business Name',
|
||||
'address': settings.address if settings else '',
|
||||
'phone': settings.phone if settings else '',
|
||||
'email': settings.email if settings else '',
|
||||
'currency': settings.currency_symbol if settings else '$',
|
||||
'vat_number': settings.vat_number if settings else '',
|
||||
'registration_number': settings.registration_number if settings else '',
|
||||
'logo_url': settings.logo.url if settings and settings.logo else ""
|
||||
}
|
||||
|
||||
sale_info = {
|
||||
'id': sale.id,
|
||||
'created_at': sale.created_at.strftime('%Y-%m-%d %H:%M'),
|
||||
'customer_name': sale.customer.name if sale.customer else 'Guest',
|
||||
'subtotal': float(sale.subtotal) if hasattr(sale, 'subtotal') else float(sale.total_amount) - float(sale.vat_amount),
|
||||
'vat_amount': float(sale.vat_amount),
|
||||
'total': float(sale.total_amount),
|
||||
'discount': float(sale.discount),
|
||||
'items': [
|
||||
{
|
||||
'name_en': item.product.name_en,
|
||||
'name_ar': item.product.name_ar,
|
||||
'qty': float(item.quantity),
|
||||
'total': float(item.line_total)
|
||||
} for item in sale.items.all().select_related('product')
|
||||
]
|
||||
}
|
||||
# Recalculate subtotal/vat if model default was 0
|
||||
total_line = sum([i['total'] for i in sale_info['items']])
|
||||
# Simple back calculation if fields aren't populated yet
|
||||
if sale_info['subtotal'] <= 0 and sale_info['total'] > 0:
|
||||
sale_info['subtotal'] = total_line
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'sale_id': sale.id,
|
||||
'business': business_info,
|
||||
'sale': sale_info
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Sale Error: {e}")
|
||||
return JsonResponse({'success': False, 'error': str(e)})
|
||||
|
||||
@csrf_exempt
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user