session summery
This commit is contained in:
parent
c8c0620ceb
commit
8f6e105aac
Binary file not shown.
Binary file not shown.
@ -19,12 +19,18 @@ def project_context(request):
|
||||
def global_settings(request):
|
||||
settings = None
|
||||
try:
|
||||
# Use a quick query to avoid hangs if DB is locked
|
||||
settings = SystemSetting.objects.first()
|
||||
if not settings:
|
||||
settings = SystemSetting.objects.create()
|
||||
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.
|
||||
# Only attempt creation if we are absolutely sure it's missing
|
||||
# and wrap it in a try-except to avoid crashes on read-only DBs
|
||||
try:
|
||||
settings = SystemSetting.objects.create(business_name="Meezan Accounting")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create SystemSetting: {e}")
|
||||
settings = None
|
||||
except Exception as e:
|
||||
logger.warning(f"Database error in global_settings: {e}")
|
||||
pass
|
||||
|
||||
return {
|
||||
|
||||
@ -27,6 +27,8 @@
|
||||
{% endif %}
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css">
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
@ -494,7 +496,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
{% if user.is_authenticated %}
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
@ -541,32 +545,6 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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>
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
{% block title %}{% trans "Login" %} - {{ site_settings.business_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container h-100 d-flex align-items-center justify-content-center">
|
||||
<div class="card glass-card border-0 shadow-lg p-4" style="max-width: 400px; width: 100%;">
|
||||
<div class="login-wrapper">
|
||||
<div class="card glass-card border-0 shadow-lg p-4 login-card">
|
||||
<div class="text-center mb-4">
|
||||
<h2 class="fw-bold text-primary">{% trans "Welcome Back" %}</h2>
|
||||
<p class="text-muted small">{% trans "Please login to access your dashboard" %}</p>
|
||||
@ -47,10 +47,24 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Center the login box specifically since we're using the base template with sidebar */
|
||||
#sidebar { display: none !important; }
|
||||
#content { margin-left: 0 !important; width: 100%; height: 100vh; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); }
|
||||
.top-navbar { display: none !important; }
|
||||
main { padding: 0 !important; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
/* Reset and centering for login page */
|
||||
#sidebar, .top-navbar { display: none !important; }
|
||||
#content { margin-left: 0 !important; width: 100%; min-height: 100vh; background-color: #f0f2f5; }
|
||||
main { padding: 0 !important; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
||||
|
||||
.login-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.login-card {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
background-color: white !important;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
121
core/views.py
121
core/views.py
@ -1688,7 +1688,24 @@ def create_lpo_api(request):
|
||||
# --- Cashier / Sessions ---
|
||||
@login_required
|
||||
def cashier_registry(request):
|
||||
registries = CashierCounterRegistry.objects.select_related('user', 'counter').all()
|
||||
if request.method == 'POST':
|
||||
action = request.POST.get('action')
|
||||
if action == 'assign':
|
||||
cashier_id = request.POST.get('cashier_id')
|
||||
counter_id = request.POST.get('counter_id')
|
||||
if cashier_id and counter_id:
|
||||
# One cashier per counter (or update existing assignment for this cashier)
|
||||
CashierCounterRegistry.objects.update_or_create(
|
||||
cashier_id=cashier_id,
|
||||
defaults={'counter_id': counter_id}
|
||||
)
|
||||
elif action == 'delete':
|
||||
registry_id = request.POST.get('registry_id')
|
||||
if registry_id:
|
||||
CashierCounterRegistry.objects.filter(id=registry_id).delete()
|
||||
return redirect('cashier_registry')
|
||||
|
||||
registries = CashierCounterRegistry.objects.select_related('cashier', 'counter').all()
|
||||
# Provide data for dropdowns
|
||||
users = User.objects.filter(is_active=True).order_by('username')
|
||||
counters = Device.objects.all().order_by('name')
|
||||
@ -1706,26 +1723,106 @@ def cashier_session_list(request):
|
||||
|
||||
@login_required
|
||||
def start_session(request):
|
||||
if request.method == 'POST':
|
||||
CashierSession.objects.create(user=request.user, opening_balance=request.POST.get('opening_balance', 0))
|
||||
# Check if user already has an active session
|
||||
active = CashierSession.objects.filter(user=request.user, status='active').first()
|
||||
if active:
|
||||
return redirect('pos')
|
||||
return render(request, 'core/start_session.html')
|
||||
|
||||
# Get assigned counter
|
||||
registry = CashierCounterRegistry.objects.filter(cashier=request.user).first()
|
||||
counter = registry.counter if registry else None
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CashierSessionStartForm(request.POST)
|
||||
if form.is_valid():
|
||||
session = form.save(commit=False)
|
||||
session.user = request.user
|
||||
session.counter = counter
|
||||
session.status = 'active'
|
||||
session.save()
|
||||
return redirect('pos')
|
||||
else:
|
||||
form = CashierSessionStartForm()
|
||||
|
||||
return render(request, 'core/start_session.html', {
|
||||
'form': form,
|
||||
'counter': counter
|
||||
})
|
||||
|
||||
@login_required
|
||||
def close_session(request):
|
||||
session = CashierSession.objects.filter(user=request.user, status='active').first()
|
||||
if request.method == 'POST' and session:
|
||||
session.closing_balance = request.POST.get('closing_balance', 0)
|
||||
session.status = 'closed'
|
||||
session.end_time = timezone.now()
|
||||
session.save()
|
||||
return redirect('index')
|
||||
return render(request, 'core/close_session.html', {'session': session})
|
||||
if not session:
|
||||
return redirect('pos')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CashierSessionCloseForm(request.POST, instance=session)
|
||||
if form.is_valid():
|
||||
session = form.save(commit=False)
|
||||
session.status = 'closed'
|
||||
session.end_time = timezone.now()
|
||||
session.save()
|
||||
return redirect('session_detail', pk=session.pk)
|
||||
else:
|
||||
form = CashierSessionCloseForm(instance=session)
|
||||
|
||||
# Statistics for the session
|
||||
end_time = timezone.now()
|
||||
payments = SalePayment.objects.filter(
|
||||
created_by=session.user,
|
||||
created_at__gte=session.start_time,
|
||||
created_at__lte=end_time
|
||||
)
|
||||
payment_summary = payments.values('payment_method_name').annotate(total=Sum('amount')).order_by('-total')
|
||||
total_sales = payments.aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
formatted_payments = []
|
||||
for p in payment_summary:
|
||||
formatted_payments.append({
|
||||
'payment_method_name': p['payment_method_name'] or _("Unknown"),
|
||||
'total': p['total']
|
||||
})
|
||||
|
||||
return render(request, 'core/close_session.html', {
|
||||
'session': session,
|
||||
'form': form,
|
||||
'payments': formatted_payments,
|
||||
'total_sales': total_sales
|
||||
})
|
||||
|
||||
@login_required
|
||||
def session_detail(request, pk):
|
||||
session = get_object_or_404(CashierSession, pk=pk)
|
||||
return render(request, 'core/session_detail.html', {'session': session})
|
||||
|
||||
# Calculate sales during this session
|
||||
# We use start_time and end_time (or now if active)
|
||||
end_time = session.end_time or timezone.now()
|
||||
|
||||
payments = SalePayment.objects.filter(
|
||||
created_by=session.user,
|
||||
created_at__gte=session.start_time,
|
||||
created_at__lte=end_time
|
||||
)
|
||||
|
||||
# Group by payment method
|
||||
payment_summary = payments.values('payment_method_name').annotate(total=Sum('amount')).order_by('-total')
|
||||
|
||||
total_sales = payments.aggregate(total=Sum('amount'))['total'] or 0
|
||||
|
||||
# Convert to expected template structure
|
||||
formatted_payments = []
|
||||
for p in payment_summary:
|
||||
formatted_payments.append({
|
||||
'payment_method_name': p['payment_method_name'] or _("Unknown"),
|
||||
'total': p['total']
|
||||
})
|
||||
|
||||
context = {
|
||||
'session': session,
|
||||
'payments': formatted_payments,
|
||||
'total_sales': total_sales,
|
||||
}
|
||||
return render(request, 'core/session_detail.html', context)
|
||||
|
||||
# --- APIs ---
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user