403 lines
14 KiB
Python
403 lines
14 KiB
Python
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'
|
|
}) |