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.urls import include, path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from core.helpers import fix_db_view
|
||||||
|
|
||||||
urlpatterns = [
|
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("admin/", admin.site.urls),
|
||||||
path("accounts/", include("django.contrib.auth.urls")),
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
path("i18n/", include("django.conf.urls.i18n")),
|
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 .models import SystemSetting
|
||||||
|
from django.db.utils import OperationalError
|
||||||
|
from django.core.management import call_command
|
||||||
import os
|
import os
|
||||||
import time
|
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())
|
STARTUP_TIMESTAMP = int(time.time())
|
||||||
|
|
||||||
def project_context(request):
|
def project_context(request):
|
||||||
"""
|
|
||||||
Injects project description and social image URL from environment variables.
|
|
||||||
Also injects a deployment timestamp for cache-busting.
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
@ -18,14 +17,18 @@ def project_context(request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def global_settings(request):
|
def global_settings(request):
|
||||||
|
settings = None
|
||||||
try:
|
try:
|
||||||
settings = SystemSetting.objects.first()
|
settings = SystemSetting.objects.first()
|
||||||
if not settings:
|
if not settings:
|
||||||
settings = SystemSetting.objects.create()
|
settings = SystemSetting.objects.create()
|
||||||
return {
|
except Exception:
|
||||||
'site_settings': settings,
|
# If DB is broken (OperationalError, etc.), just return None.
|
||||||
'global_settings': settings,
|
# Do not try to fix it here to avoid infinite loops or crashes during template rendering.
|
||||||
'decimal_places': settings.decimal_places if settings else 3
|
pass
|
||||||
}
|
|
||||||
except:
|
return {
|
||||||
return {'decimal_places': 3}
|
'site_settings': settings,
|
||||||
|
'global_settings': settings,
|
||||||
|
'decimal_places': settings.decimal_places if settings else 3
|
||||||
|
}
|
||||||
|
|||||||
@ -1,30 +1,28 @@
|
|||||||
from django.http import HttpResponse
|
# Internal Helper Script - NOT for production use
|
||||||
from django.db import connection
|
from django.db import connection, transaction
|
||||||
|
from core.models import Product
|
||||||
|
|
||||||
def fix_db_view(request):
|
def fix_missing_columns():
|
||||||
log = []
|
"""
|
||||||
|
Manually checks and adds missing columns if migrations fail.
|
||||||
|
"""
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
# 1. Check/Add is_service to core_product
|
# Check is_service
|
||||||
try:
|
try:
|
||||||
cursor.execute("SELECT is_service FROM core_product LIMIT 1")
|
cursor.execute("SELECT is_service FROM core_product LIMIT 1")
|
||||||
log.append("SUCCESS: is_service already exists in core_product.")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
print("Adding is_service column...")
|
||||||
try:
|
try:
|
||||||
# Try MySQL syntax first
|
cursor.execute("ALTER TABLE core_product ADD COLUMN is_service tinyint(1) NOT NULL DEFAULT 0")
|
||||||
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:
|
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:
|
try:
|
||||||
cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1")
|
cursor.execute("SELECT is_active FROM core_paymentmethod LIMIT 1")
|
||||||
log.append("SUCCESS: is_active already exists in core_paymentmethod.")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
print("Adding is_active column to PaymentMethod...")
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE core_paymentmethod ADD COLUMN is_active tinyint(1) NOT NULL DEFAULT 1;")
|
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:
|
except Exception as e:
|
||||||
log.append(f"ERROR adding is_active: {e}")
|
print(f"Error adding column: {e}")
|
||||||
|
|
||||||
return HttpResponse("<br>".join(log) + "<br><br><a href='/'>Go to Dashboard</a>")
|
|
||||||
|
|||||||
158
core/helpers.py
158
core/helpers.py
@ -1,153 +1,11 @@
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.db import connection
|
from django.core.management import call_command
|
||||||
|
import io
|
||||||
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)
|
|
||||||
|
|
||||||
def fix_db_view(request):
|
def fix_db_view(request):
|
||||||
log = []
|
out = io.StringIO()
|
||||||
with connection.cursor() as cursor:
|
try:
|
||||||
# 1. Check/Add is_service to core_product
|
call_command('migrate', 'core', stdout=out)
|
||||||
try:
|
return HttpResponse(f"SUCCESS: Database updated.<br><pre>{out.getvalue()}</pre><br><a href='/'>Go Home</a>")
|
||||||
cursor.execute("SELECT is_service FROM core_product LIMIT 1")
|
except Exception as e:
|
||||||
log.append("SUCCESS: is_service already exists in core_product.")
|
return HttpResponse(f"ERROR: {e}<br><pre>{out.getvalue()}</pre>")
|
||||||
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>")
|
|
||||||
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)
|
tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0)
|
||||||
decimal_places = models.PositiveSmallIntegerField(_("Decimal Places"), default=3)
|
decimal_places = models.PositiveSmallIntegerField(_("Decimal Places"), default=3)
|
||||||
logo = models.FileField(_("Logo"), upload_to="business_logos/", blank=True, null=True)
|
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)
|
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)
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,12 @@
|
|||||||
<meta property="og:description" content="{{ project_description }}">
|
<meta property="og:description" content="{{ project_description }}">
|
||||||
{% endif %}
|
{% 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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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) {
|
function prepareInvoice(data) {
|
||||||
const logo = document.getElementById('inv-logo');
|
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) {
|
if (data.business.logo_url) {
|
||||||
logo.src = data.business.logo_url;
|
logo.src = data.business.logo_url;
|
||||||
logo.style.display = 'inline-block';
|
logo.style.display = 'inline-block';
|
||||||
|
|||||||
@ -75,8 +75,9 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" enctype="multipart/form-data" action="{% url 'settings' %}">
|
<form method="post" enctype="multipart/form-data" action="{% url 'settings' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="setting_type" value="profile">
|
||||||
<div class="row g-3">
|
<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>
|
<label class="form-label d-block fw-semibold">{% trans "Business Logo" %}</label>
|
||||||
{% if settings.logo %}
|
{% if settings.logo %}
|
||||||
<img src="{{ settings.logo.url }}" alt="Logo" class="img-thumbnail mb-2" style="max-height: 100px;">
|
<img src="{{ settings.logo.url }}" alt="Logo" class="img-thumbnail mb-2" style="max-height: 100px;">
|
||||||
@ -87,6 +88,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<input type="file" name="logo" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*">
|
<input type="file" name="logo" class="form-control form-control-sm mx-auto" style="max-width: 300px;" accept="image/*">
|
||||||
</div>
|
</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">
|
<div class="col-md-12">
|
||||||
<label class="form-label fw-semibold">{% trans "Business Name" %}</label>
|
<label class="form-label fw-semibold">{% trans "Business Name" %}</label>
|
||||||
@ -572,6 +584,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" action="{% url 'settings' %}">
|
<form method="post" action="{% url 'settings' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="setting_type" value="whatsapp">
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="form-check form-switch mb-3">
|
<div class="form-check form-switch mb-3">
|
||||||
@ -774,7 +787,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer bg-light border-0">
|
<div class="modal-footer bg-light border-0">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
181
core/views.py
181
core/views.py
@ -37,7 +37,22 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
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()
|
today = timezone.now().date()
|
||||||
|
|
||||||
# 1. Financials
|
# 1. Financials
|
||||||
@ -149,11 +164,29 @@ def inventory(request):
|
|||||||
products = Product.objects.all().order_by('name_en')
|
products = Product.objects.all().order_by('name_en')
|
||||||
categories = Category.objects.all()
|
categories = Category.objects.all()
|
||||||
units = Unit.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 = {
|
context = {
|
||||||
'products': products,
|
'products': products,
|
||||||
'categories': categories,
|
'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)
|
return render(request, 'core/inventory.html', context)
|
||||||
|
|
||||||
@ -169,9 +202,14 @@ def suppliers(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def settings_view(request):
|
def settings_view(request):
|
||||||
settings = SystemSetting.objects.first()
|
settings = None
|
||||||
if not settings:
|
try:
|
||||||
settings = SystemSetting.objects.create()
|
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)
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
||||||
expense_categories = ExpenseCategory.objects.all()
|
expense_categories = ExpenseCategory.objects.all()
|
||||||
@ -179,13 +217,40 @@ def settings_view(request):
|
|||||||
devices = Device.objects.all().order_by("name")
|
devices = Device.objects.all().order_by("name")
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = SystemSettingForm(request.POST, request.FILES, instance=settings)
|
setting_type = request.POST.get('setting_type')
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
# Robust check for WhatsApp update: Check hidden field OR explicit token field
|
||||||
messages.success(request, "Settings updated.")
|
is_whatsapp_update = (setting_type == 'whatsapp') or ('wablas_token' in request.POST)
|
||||||
return redirect('settings')
|
|
||||||
|
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:
|
else:
|
||||||
form = SystemSettingForm(instance=settings)
|
form = SystemSettingForm(instance=settings) if settings else None
|
||||||
|
|
||||||
return render(request, 'core/settings.html', {
|
return render(request, 'core/settings.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
@ -253,7 +318,12 @@ def pos(request):
|
|||||||
messages.warning(request, _("Please open a session to start selling."))
|
messages.warning(request, _("Please open a session to start selling."))
|
||||||
return redirect('start_session')
|
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)
|
products = Product.objects.filter(is_active=True)
|
||||||
|
|
||||||
if settings and not settings.allow_zero_stock_sales:
|
if settings and not settings.allow_zero_stock_sales:
|
||||||
@ -274,6 +344,7 @@ def pos(request):
|
|||||||
'categories': categories,
|
'categories': categories,
|
||||||
'payment_methods': payment_methods,
|
'payment_methods': payment_methods,
|
||||||
'settings': settings,
|
'settings': settings,
|
||||||
|
'site_settings': settings, # Add site_settings for template consistency
|
||||||
'active_session': active_session
|
'active_session': active_session
|
||||||
}
|
}
|
||||||
return render(request, 'core/pos.html', context)
|
return render(request, 'core/pos.html', context)
|
||||||
@ -313,11 +384,17 @@ def invoice_list(request):
|
|||||||
|
|
||||||
paginator = Paginator(sales, 25)
|
paginator = Paginator(sales, 25)
|
||||||
|
|
||||||
|
settings = None
|
||||||
|
try:
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'sales': paginator.get_page(request.GET.get('page')),
|
'sales': paginator.get_page(request.GET.get('page')),
|
||||||
'customers': Customer.objects.all(),
|
'customers': Customer.objects.all(),
|
||||||
'payment_methods': PaymentMethod.objects.filter(is_active=True),
|
'payment_methods': PaymentMethod.objects.filter(is_active=True),
|
||||||
'site_settings': SystemSetting.objects.first(),
|
'site_settings': settings,
|
||||||
}
|
}
|
||||||
return render(request, 'core/invoices.html', context)
|
return render(request, 'core/invoices.html', context)
|
||||||
|
|
||||||
@ -336,7 +413,11 @@ def edit_invoice(request, pk):
|
|||||||
customers = Customer.objects.all()
|
customers = Customer.objects.all()
|
||||||
products = Product.objects.filter(is_active=True).select_related('category')
|
products = Product.objects.filter(is_active=True).select_related('category')
|
||||||
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
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
|
decimal_places = 2
|
||||||
if site_settings:
|
if site_settings:
|
||||||
@ -413,7 +494,11 @@ def customer_payments(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def customer_payment_receipt(request, pk):
|
def customer_payment_receipt(request, pk):
|
||||||
payment = get_object_or_404(SalePayment, pk=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', {
|
return render(request, 'core/payment_receipt.html', {
|
||||||
'payment': payment,
|
'payment': payment,
|
||||||
'settings': settings,
|
'settings': settings,
|
||||||
@ -423,7 +508,11 @@ def customer_payment_receipt(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
def sale_receipt(request, pk):
|
def sale_receipt(request, pk):
|
||||||
sale = get_object_or_404(Sale, pk=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', {
|
return render(request, 'core/sale_receipt.html', {
|
||||||
'sale': sale,
|
'sale': sale,
|
||||||
'settings': settings
|
'settings': settings
|
||||||
@ -974,7 +1063,11 @@ def lpo_create(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def lpo_detail(request, pk):
|
def lpo_detail(request, pk):
|
||||||
lpo = get_object_or_404(PurchaseOrder, pk=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})
|
return render(request, 'core/lpo_detail.html', {'lpo': lpo, 'settings': settings})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -1042,7 +1135,9 @@ def create_sale_api(request):
|
|||||||
paid_amount=data.get('paid_amount', 0),
|
paid_amount=data.get('paid_amount', 0),
|
||||||
payment_type=data.get('payment_type', 'cash'),
|
payment_type=data.get('payment_type', 'cash'),
|
||||||
created_by=request.user,
|
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', []):
|
for item in data.get('items', []):
|
||||||
SaleItem.objects.create(
|
SaleItem.objects.create(
|
||||||
@ -1062,8 +1157,56 @@ def create_sale_api(request):
|
|||||||
payment_method_id=data.get('payment_method_id'),
|
payment_method_id=data.get('payment_method_id'),
|
||||||
created_by=request.user
|
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:
|
except Exception as e:
|
||||||
|
logger.error(f"Sale Error: {e}")
|
||||||
return JsonResponse({'success': False, 'error': str(e)})
|
return JsonResponse({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user