more modification

This commit is contained in:
Flatlogic Bot 2026-02-11 16:45:13 +00:00
parent e405332220
commit 2a2b761270
8 changed files with 216 additions and 7 deletions

9
.gitignore vendored
View File

@ -1,3 +1,12 @@
node_modules/
*/node_modules/
*/build/
.env
db.sqlite3
media/
staticfiles/
__pycache__/
*.pyc
.DS_Store
*.log
tmp/

View File

@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2026-02-11 16:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0035_remove_heldsale_customer_remove_heldsale_notes_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='systemsetting',
options={'permissions': [('view_dashboard', 'Can view dashboard'), ('view_pos', 'Can access POS'), ('view_reports', 'Can view reports'), ('view_accounting', 'Can view accounting'), ('view_hr', 'Can view HR')]},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2026-02-11 16:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0036_alter_systemsetting_options'),
]
operations = [
migrations.AlterModelOptions(
name='systemsetting',
options={'permissions': [('view_dashboard', 'Can view dashboard'), ('view_pos', 'Can access POS'), ('view_reports', 'Can view reports'), ('view_accounting', 'Can view accounting'), ('view_hr', 'Can view HR'), ('view_inventory', 'Can view inventory'), ('view_sales', 'Can view sales'), ('view_purchases', 'Can view purchases'), ('view_customers', 'Can view customers'), ('view_suppliers', 'Can view suppliers'), ('view_expenses', 'Can view expenses'), ('view_lpo', 'Can view LPO'), ('view_quotations', 'Can view quotations')]},
),
]

View File

@ -399,6 +399,21 @@ 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)
# 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)
currency_per_point = models.DecimalField(_("Currency Value per Point"), max_digits=10, decimal_places=3, default=0.01)
min_points_to_redeem = models.PositiveIntegerField(_("Minimum Points to Redeem"), default=100)
# WhatsApp Gateway (Wablas)
wablas_enabled = models.BooleanField(_("Enable WhatsApp Gateway"), default=False)
wablas_server_url = models.URLField(_("Wablas Server URL"), blank=True, help_text=_("Example: https://console.wablas.com"))
wablas_token = models.CharField(_("Wablas API Token"), max_length=255, blank=True)
wablas_secret_key = models.CharField(_("Wablas Secret Key"), max_length=255, blank=True)
# POS Settings
allow_zero_stock_sales = models.BooleanField(_("Allow selling items with 0 stock"), default=False)
class Meta:
permissions = [
("view_dashboard", "Can view dashboard"),
@ -406,6 +421,14 @@ class SystemSetting(models.Model):
("view_reports", "Can view reports"),
("view_accounting", "Can view accounting"),
("view_hr", "Can view HR"),
("view_inventory", "Can view inventory"),
("view_sales", "Can view sales"),
("view_purchases", "Can view purchases"),
("view_customers", "Can view customers"),
("view_suppliers", "Can view suppliers"),
("view_expenses", "Can view expenses"),
("view_lpo", "Can view LPO"),
("view_quotations", "Can view quotations"),
]
def __str__(self):

View File

@ -64,11 +64,14 @@
<i class="bi bi-chevron-down chevron"></i>
</a>
<ul class="collapse list-unstyled sub-menu {% if url_name == 'pos' or url_name == 'invoice_create' or url_name == 'invoices' or url_name == 'invoice_detail' or url_name == 'quotations' or url_name == 'quotation_create' or url_name == 'quotation_detail' or 'sales/returns' in path %}show{% endif %}" id="salesSubmenu">
{% if user.is_staff or perms.core.view_pos %}
<li>
<a href="{% url 'pos' %}" class="{% if url_name == 'pos' %}active{% endif %}">
<i class="bi bi-shop"></i> {% trans "POS System" %}
</a>
</li>
{% endif %}
{% if user.is_staff or perms.core.view_sales %}
<li>
<a href="{% url 'invoice_create' %}" class="{% if url_name == 'invoice_create' %}active{% endif %}">
<i class="bi bi-plus-circle"></i> {% trans "New Sales" %}
@ -79,16 +82,21 @@
<i class="bi bi-file-earmark-text"></i> {% trans "Sales Invoices" %}
</a>
</li>
{% endif %}
{% if user.is_staff or perms.core.view_quotations %}
<li>
<a href="{% url 'quotations' %}" class="{% if url_name == 'quotations' or url_name == 'quotation_create' or url_name == 'quotation_detail' %}active{% endif %}">
<i class="bi bi-file-earmark-spreadsheet"></i> {% trans "Quotation" %}
</a>
</li>
{% endif %}
{% if user.is_staff or perms.core.view_sales %}
<li>
<a href="{% url 'sales_returns' %}" class="{% if 'sales/returns' in path %}active{% endif %}">
<i class="bi bi-arrow-return-left"></i> {% trans "Sales Return" %}
</a>
</li>
{% endif %}
</ul>
</li>
@ -99,11 +107,14 @@
<i class="bi bi-chevron-down chevron"></i>
</a>
<ul class="collapse list-unstyled sub-menu {% if url_name == 'purchases' or url_name == 'purchase_create' or url_name == 'purchase_detail' or url_name == 'supplier_payments' or 'purchases/returns' in path %}show{% endif %}" id="purchasesSubmenu">
{% if user.is_staff or perms.core.view_lpo %}
<li>
<a href="{% url 'lpo_list' %}" class="{% if url_name == 'lpo_list' or url_name == 'lpo_create' or url_name == 'lpo_detail' %}active{% endif %}">
<i class="bi bi-file-earmark-text"></i> {% trans "Purchase Orders (LPO)" %}
</a>
</li>
{% endif %}
{% if user.is_staff or perms.core.view_purchases %}
<li>
<a href="{% url 'purchase_create' %}" class="{% if url_name == 'purchase_create' %}active{% endif %}">
<i class="bi bi-plus-circle"></i> {% trans "New Purchase" %}
@ -124,6 +135,7 @@
<i class="bi bi-arrow-return-right"></i> {% trans "Purchase Return" %}
</a>
</li>
{% endif %}
</ul>
</li>
@ -134,6 +146,7 @@
<i class="bi bi-chevron-down chevron"></i>
</a>
<ul class="collapse list-unstyled sub-menu {% if url_name == 'inventory' or url_name == 'barcode_labels' or url_name == 'reports' %}show{% endif %}" id="inventorySubmenu">
{% if user.is_staff or perms.core.view_inventory %}
<li>
<a href="{% url 'inventory' %}" class="{% if url_name == 'inventory' %}active{% endif %}">
<i class="bi bi-box-seam"></i> {% trans "Products" %}
@ -144,7 +157,7 @@
<i class="bi bi-upc-scan"></i> {% trans "Barcode Printing" %}
</a>
</li>
{% endif %}
</ul>
</li>
@ -155,6 +168,7 @@
<i class="bi bi-chevron-down chevron"></i>
</a>
<ul class="collapse list-unstyled sub-menu {% if url_name == 'expenses' or url_name == 'expense_categories' %}show{% endif %}" id="expensesSubmenu">
{% if user.is_staff or perms.core.view_expenses %}
<li>
<a href="{% url 'expenses' %}" class="{% if url_name == 'expenses' %}active{% endif %}">
<i class="bi bi-receipt"></i> {% trans "Expense List" %}
@ -165,6 +179,7 @@
<i class="bi bi-tags"></i> {% trans "Categories" %}
</a>
</li>
{% endif %}
</ul>
</li>
@ -175,21 +190,24 @@
<i class="bi bi-chevron-down chevron"></i>
</a>
<ul class="collapse list-unstyled sub-menu {% if url_name == 'customers' or url_name == 'suppliers' %}show{% endif %}" id="contactsSubmenu">
{% if user.is_staff or perms.core.view_customers %}
<li>
<a href="{% url 'customers' %}" class="{% if url_name == 'customers' %}active{% endif %}">
<i class="bi bi-people"></i> {% trans "Customers" %}
</a>
</li>
{% endif %}
{% if user.is_staff or perms.core.view_suppliers %}
<li>
<a href="{% url 'suppliers' %}" class="{% if url_name == 'suppliers' %}active{% endif %}">
<i class="bi bi-truck"></i> {% trans "Suppliers" %}
</a>
</li>
{% endif %}
</ul>
</li>
{% if user.is_superuser or user.is_staff %}
{% if user.is_staff or perms.core.view_accounting %}
<!-- Accounting Group -->
<li class="sidebar-group-header mt-1">
<a href="#accountingSubmenu" data-bs-toggle="collapse" aria-expanded="{% if 'accounting' in path %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
@ -229,7 +247,9 @@
</li>
</ul>
</li>
{% endif %}
{% if user.is_staff or perms.core.view_hr %}
<!-- HR Group -->
<li class="sidebar-group-header mt-1">
<a href="#hrSubmenu" data-bs-toggle="collapse" aria-expanded="{% if 'hr/' in path %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
@ -269,7 +289,9 @@
</li>
</ul>
</li>
{% endif %}
{% if user.is_staff or perms.core.view_reports %}
<!-- Reports Group -->
<li class="sidebar-group-header mt-1">
<a href="#reportsSubmenu" data-bs-toggle="collapse" aria-expanded="{% if url_name == 'reports' or url_name == 'customer_statement' or url_name == 'supplier_statement' or url_name == 'cashflow_report' or url_name == 'expense_report' %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
@ -304,7 +326,9 @@
</li>
</ul>
</li>
{% endif %}
{% if user.is_staff %}
<!-- System Group -->
<li class="sidebar-group-header mt-1">
<a href="#systemSubmenu" data-bs-toggle="collapse" aria-expanded="{% if url_name == 'settings' or url_name == 'user_management' or url_name == 'cashier_registry' or '/admin/' in path %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
@ -378,6 +402,17 @@
</button>
<div class="d-flex align-items-center gap-3">
{% if messages %}
<div class="me-3">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mb-0 py-1 px-3 shadow-sm border-0" role="alert" style="font-size: 0.85rem;">
{{ message }}
<button type="button" class="btn-close py-2" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
<div class="language-switcher">
<form action="{% url 'set_language' %}" method="post" class="d-flex align-items-center">
{% csrf_token %}
@ -478,6 +513,11 @@
document.addEventListener('submit', function (e) {
const form = e.target;
if (form.tagName === 'FORM') {
// Don't disable if it's the login form or has a specific class
if (form.action.includes('login') || form.classList.contains('no-disable')) {
return;
}
if (form.getAttribute('data-submitted') === 'true') {
e.preventDefault();
return;
@ -486,7 +526,6 @@
const submitButtons = form.querySelectorAll('button[type="submit"], input[type="submit"]');
submitButtons.forEach(btn => {
btn.disabled = true;
// Add spinner if it is a button and not already there
if (btn.tagName === 'BUTTON' && !btn.querySelector('.spinner-border')) {
const originalText = btn.innerHTML;
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${originalText}`;
@ -494,6 +533,32 @@
});
}
});
// Cleanup stale modal backdrops and body classes aggressively
(function() {
function cleanupModals() {
const backdrops = document.querySelectorAll('.modal-backdrop');
if (backdrops.length > 0) {
console.log('Cleaning up ' + backdrops.length + ' stale backdrops');
backdrops.forEach(el => el.remove());
}
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
}
// Run multiple times to ensure cleanup
cleanupModals();
window.addEventListener('DOMContentLoaded', cleanupModals);
window.addEventListener('load', cleanupModals);
// Periodically check for 2 seconds
let attempts = 0;
const interval = setInterval(() => {
cleanupModals();
if (++attempts > 10) clearInterval(interval);
}, 500);
})();
</script>
{% block scripts %}{% endblock %}
</body>

View File

@ -391,6 +391,11 @@ def group_details_api(request, pk):
@login_required
def pos(request):
# Permission check
if not (request.user.is_staff or request.user.has_perm('core.view_pos')):
messages.error(request, _("You do not have permission to access the POS system."))
return redirect('index')
# Check for active session
active_session = CashierSession.objects.filter(user=request.user, status='active').first()
if not active_session:
@ -2042,18 +2047,91 @@ def get_customer_loyalty_api(request, pk):
@csrf_exempt
@login_required
def hold_sale_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
customer_name = ""
customer_id = data.get('customer_id')
if customer_id:
try:
customer = Customer.objects.get(id=customer_id)
customer_name = customer.name
except (Customer.DoesNotExist, ValueError):
pass
# Store everything in cart_data JSON
cart_info = {
'items': data.get('items', []),
'customer_id': customer_id,
'customer_name': customer_name
}
HeldSale.objects.create(
created_by=request.user,
cart_data=json.dumps(cart_info),
customer_name=customer_name,
note=data.get('notes', "")
)
return JsonResponse({'success': True})
except Exception as e:
logger.error(f"Error holding sale: {str(e)}")
return JsonResponse({'success': False, 'error': str(e)})
return JsonResponse({'success': False, 'error': 'Only POST allowed'})
@login_required
def get_held_sales_api(request):
return JsonResponse({'sales': []})
sales = HeldSale.objects.filter(created_by=request.user).order_by('-created_at')
sales_list = []
for s in sales:
try:
cart_info = json.loads(s.cart_data)
item_count = len(cart_info.get('items', []))
except:
item_count = 0
sales_list.append({
'id': s.id,
'customer_name': s.customer_name or _("Walk-in Customer"),
'note': s.note,
'created_at': s.created_at.strftime('%Y-%m-%d %H:%M'),
'item_count': item_count
})
return JsonResponse({'success': True, 'held_sales': sales_list})
@login_required
def recall_held_sale_api(request, pk):
return JsonResponse({'success': True})
sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
try:
cart_info = json.loads(sale.cart_data)
# Support both old format (just items) and new format (dict with items, customer_id, etc)
if isinstance(cart_info, list):
items = cart_info
customer_id = ""
customer_name = sale.customer_name
else:
items = cart_info.get('items', [])
customer_id = cart_info.get('customer_id', "")
customer_name = cart_info.get('customer_name', sale.customer_name)
data = {
'success': True,
'items': items,
'customer_id': customer_id,
'customer_name': customer_name,
}
# Delete the held sale once recalled
sale.delete()
return JsonResponse(data)
except Exception as e:
logger.error(f"Error recalling sale: {str(e)}")
return JsonResponse({'success': False, 'error': str(e)})
@login_required
def delete_held_sale_api(request, pk):
sale = get_object_or_404(HeldSale, pk=pk, created_by=request.user)
sale.delete()
return JsonResponse({'success': True})
@login_required
def backup_database(request):