import os import platform import io from datetime import timedelta from django import get_version as django_version from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone from django.db.models import Sum, Q from django.contrib.auth.decorators import login_required from django.contrib import messages from django.http import JsonResponse, HttpResponse from .models import Medicine, Batch, Category, StockTransaction, Supplier, Faktur from .forms import SupplierForm, FakturForm, StockInForm, StockOutForm, CategoryForm, MedicineForm from reportlab.lib import colors from reportlab.lib.pagesizes import letter, A4 from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle @login_required def home(request): """Render the medicine warehouse dashboard.""" now = timezone.now() today = now.date() # Stats total_medicines = Medicine.objects.count() # Total stock across all batches total_stock = Batch.objects.aggregate(total=Sum('quantity'))['total'] or 0 # Expired batches expired_batches_count = Batch.objects.filter(expiry_date__lte=today).count() # Near expiry (next 90 days) near_expiry_batches_count = Batch.objects.filter( expiry_date__gt=today, expiry_date__lte=today + timezone.timedelta(days=90) ).count() # Low stock medicines all_medicines = Medicine.objects.all() low_stock_count = 0 for med in all_medicines: if med.total_stock <= med.min_stock: low_stock_count += 1 # Latest medicines for the table recent_medicines = all_medicines.order_by('-created_at')[:5] # Chart Data: Last 7 days movement chart_labels = [] chart_data_in = [] chart_data_out = [] for i in range(6, -1, -1): day = today - timedelta(days=i) chart_labels.append(day.strftime('%d %b')) in_sum = StockTransaction.objects.filter( transaction_type='IN', created_at__date=day ).aggregate(total=Sum('quantity'))['total'] or 0 out_sum = StockTransaction.objects.filter( transaction_type='OUT', created_at__date=day ).aggregate(total=Sum('quantity'))['total'] or 0 chart_data_in.append(in_sum) chart_data_out.append(out_sum) context = { "project_name": "DN-WRS", "total_medicines": total_medicines, "total_stock": total_stock, "expired_count": expired_batches_count, "near_expiry_count": near_expiry_batches_count, "low_stock_count": low_stock_count, "recent_medicines": recent_medicines, "chart_labels": chart_labels, "chart_data_in": chart_data_in, "chart_data_out": chart_data_out, "django_version": django_version(), "python_version": platform.python_version(), "current_time": now, } return render(request, "core/index.html", context) # --- MASTER DATA MANAGEMENT --- @login_required def category_list(request): if request.method == 'POST': form = CategoryForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Kategori berhasil ditambahkan.") return redirect('category_list') else: form = CategoryForm() categories = Category.objects.all().order_by('name') return render(request, 'core/categories.html', { 'categories': categories, 'form': form, 'project_name': 'DN-WRS' }) @login_required def supplier_list(request): if request.method == 'POST': form = SupplierForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Supplier berhasil ditambahkan.") return redirect('supplier_list') else: form = SupplierForm() suppliers = Supplier.objects.all().order_by('name') return render(request, 'core/suppliers.html', { 'suppliers': suppliers, 'form': form, 'project_name': 'DN-WRS' }) @login_required def medicine_list(request): query = request.GET.get('q') show_low_stock = request.GET.get('low_stock') == '1' if request.method == 'POST': form = MedicineForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Data barang berhasil ditambahkan.") return redirect('medicine_list') else: form = MedicineForm() medicines_all = Medicine.objects.all().order_by('name') # Calculate low stock count for the summary low_stock_count = 0 for med in medicines_all: if med.total_stock <= med.min_stock: low_stock_count += 1 medicines = medicines_all if query: medicines = medicines.filter( Q(name__icontains=query) | Q(sku__icontains=query) | Q(category__name__icontains=query) ) if show_low_stock: # Filter for low stock low_stock_ids = [m.id for m in medicines if m.total_stock <= m.min_stock] medicines = medicines.filter(id__in=low_stock_ids) return render(request, 'core/medicines.html', { 'medicines': medicines, 'form': form, 'project_name': 'DN-WRS', 'query': query, 'show_low_stock': show_low_stock, 'low_stock_count': low_stock_count, 'total_count': medicines_all.count() }) @login_required def export_low_stock_pdf(request): # Get low stock medicines all_medicines = Medicine.objects.all().order_by('main_supplier__name', 'name') low_stock_medicines = [m for m in all_medicines if m.total_stock <= m.min_stock] # Group by main supplier grouped = {} for m in low_stock_medicines: supplier_name = m.main_supplier.name if m.main_supplier else "Tanpa Supplier Utama" if supplier_name not in grouped: grouped[supplier_name] = [] grouped[supplier_name].append(m) # Generate PDF buffer = io.BytesIO() doc = SimpleDocTemplate(buffer, pagesize=A4) elements = [] styles = getSampleStyleSheet() title_style = ParagraphStyle( 'Title', parent=styles['Heading1'], fontSize=18, alignment=1, spaceAfter=20, textColor=colors.HexColor("#0d6efd") ) elements.append(Paragraph("Laporan Daftar Stok Menipis", title_style)) elements.append(Paragraph(f"Tanggal Cetak: {timezone.now().strftime('%d %B %Y %H:%M')}", styles["Normal"])) elements.append(Spacer(1, 20)) if not low_stock_medicines: elements.append(Paragraph("Tidak ada stok yang menipis saat ini.", styles["Normal"])) else: for supplier, items in grouped.items(): elements.append(Paragraph(f"Supplier Utama: {supplier}", styles["Heading2"])) elements.append(Spacer(1, 5)) data = [["Nama Barang", "SKU", "Stok", "Min.", "Satuan", "Supplier Alternatif"]] for item in items: alt_supplier = item.alternative_supplier.name if item.alternative_supplier else "-" data.append([ item.name, item.sku, str(item.total_stock), str(item.min_stock), item.unit, alt_supplier ]) table = Table(data, colWidths=[160, 60, 40, 40, 50, 110]) table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#343a40")), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 10), ('BOTTOMPADDING', (0, 0), (-1, 0), 10), ('TOPPADDING', (0, 0), (-1, 0), 10), ('GRID', (0, 0), (-1, -1), 0.5, colors.grey), ('FONTSIZE', (0, 1), (-1, -1), 9), ])) elements.append(table) elements.append(Spacer(1, 15)) doc.build(elements) buffer.seek(0) response = HttpResponse(buffer, content_type='application/pdf') response['Content-Disposition'] = 'attachment; filename="laporan_stok_menipis.pdf"' return response # --- TRANSAKSI --- @login_required def input_faktur(request): if request.method == 'POST': faktur_form = FakturForm(request.POST) if faktur_form.is_valid(): faktur = faktur_form.save() return redirect('faktur_detail', pk=faktur.pk) else: faktur_form = FakturForm(initial={'faktur_type': 'MASUK', 'date': timezone.now().date()}) fakturs = Faktur.objects.all().order_by('-created_at') return render(request, 'core/input_faktur.html', { 'faktur_form': faktur_form, 'fakturs': fakturs, 'project_name': 'DN-WRS' }) @login_required def faktur_detail(request, pk): faktur = get_object_or_404(Faktur, pk=pk) if request.method == 'POST': form = StockInForm(request.POST) if form.is_valid(): # Create Batch batch = Batch.objects.create( medicine=form.cleaned_data['medicine'], faktur=faktur, batch_number=form.cleaned_data['batch_number'], expiry_date=form.cleaned_data['expiry_date'], quantity=form.cleaned_data['quantity'], buying_price=form.cleaned_data['buying_price'], selling_price=form.cleaned_data['selling_price'] ) # Create Transaction StockTransaction.objects.create( medicine=batch.medicine, batch=batch, faktur=faktur, transaction_type='IN', quantity=batch.quantity, note=f"Input dari Faktur {faktur.faktur_number}" ) messages.success(request, f"Barang {batch.medicine.name} berhasil ditambahkan.") return redirect('faktur_detail', pk=pk) else: form = StockInForm() items = Batch.objects.filter(faktur=faktur) return render(request, 'core/faktur_detail.html', { 'faktur': faktur, 'form': form, 'items': items, 'project_name': 'DN-WRS' }) @login_required def barang_keluar(request): if request.method == 'POST': form = StockOutForm(request.POST) if form.is_valid(): medicine = form.cleaned_data['medicine'] batch = form.cleaned_data['batch'] qty = form.cleaned_data['quantity'] if batch.quantity < qty: messages.error(request, f"Stok tidak mencukupi. Stok saat ini: {batch.quantity}") else: # Update Batch batch.quantity -= qty batch.save() # Create Transaction StockTransaction.objects.create( medicine=medicine, batch=batch, transaction_type='OUT', quantity=qty, note=form.cleaned_data['note'] ) messages.success(request, f"Barang keluar berhasil dicatat.") return redirect('barang_keluar') else: form = StockOutForm() transactions = StockTransaction.objects.filter(transaction_type='OUT').order_by('-created_at')[:10] return render(request, 'core/barang_keluar.html', { 'form': form, 'transactions': transactions, 'project_name': 'DN-WRS' }) @login_required def get_batches(request): medicine_id = request.GET.get('medicine_id') batches = Batch.objects.filter(medicine_id=medicine_id, quantity__gt=0).values('id', 'batch_number', 'quantity') return JsonResponse(list(batches), safe=False) @login_required def laporan_transaksi(request): transactions = StockTransaction.objects.all().order_by('-created_at') return render(request, 'core/laporan_transaksi.html', { 'transactions': transactions, 'project_name': 'DN-WRS' }) @login_required def delete_transaksi(request, pk): transaction = get_object_or_404(StockTransaction, pk=pk) batch = transaction.batch if batch: if transaction.transaction_type == 'IN': batch.quantity -= transaction.quantity elif transaction.transaction_type == 'OUT': batch.quantity += transaction.quantity batch.save() transaction.delete() messages.success(request, "Transaksi berhasil dihapus dan stok telah diperbarui.") return redirect('laporan_transaksi') @login_required def edit_transaksi(request, pk): transaction = get_object_or_404(StockTransaction, pk=pk) if request.method == 'POST': new_qty = int(request.POST.get('quantity')) old_qty = transaction.quantity batch = transaction.batch if batch: if transaction.transaction_type == 'IN': # Revert old, apply new batch.quantity = batch.quantity - old_qty + new_qty elif transaction.transaction_type == 'OUT': # Revert old, apply new batch.quantity = batch.quantity + old_qty - new_qty if batch.quantity < 0: messages.error(request, "Error: Stok tidak boleh negatif setelah perubahan.") return redirect('laporan_transaksi') batch.save() transaction.quantity = new_qty transaction.note = request.POST.get('note', transaction.note) transaction.save() messages.success(request, "Transaksi berhasil diperbarui.") return redirect('laporan_transaksi') return render(request, 'core/edit_transaksi.html', { 'transaction': transaction, 'project_name': 'DN-WRS' })