38240-vm/core/views.py
Flatlogic Bot 04c4db511d DN V1
2026-02-06 12:02:27 +00:00

621 lines
22 KiB
Python

import os
import platform
import io
import base64
import json
import pandas as pd
from datetime import timedelta, datetime
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, AppSetting
from .forms import SupplierForm, FakturForm, StockInForm, StockOutForm, CategoryForm, MedicineForm, AppSettingForm
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
from ai.local_ai_api import LocalAIApi
@login_required
def home(request):
"""Render the medicine warehouse dashboard."""
now = timezone.now()
today = now.date()
settings = AppSetting.objects.first()
project_name = settings.app_name if settings else "DN-WRS"
# 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": project_name,
"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)
@login_required
def app_settings(request):
settings = AppSetting.objects.first()
if not settings:
settings = AppSetting.objects.create(app_name="DN-WRS")
if request.method == 'POST':
form = AppSettingForm(request.POST, instance=settings)
if form.is_valid():
form.save()
messages.success(request, "Pengaturan aplikasi berhasil diperbarui.")
return redirect('app_settings')
else:
form = AppSettingForm(instance=settings)
return render(request, 'core/settings.html', {'form': form, 'settings': settings})
# --- 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
})
@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
})
@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,
'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
})
@login_required
def faktur_detail(request, pk):
faktur = get_object_or_404(Faktur, pk=pk)
if request.method == 'POST':
if 'add_item' in request.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)
cart = request.session.get('import_cart', [])
cart_faktur_id = request.session.get('cart_faktur_id')
# Only show cart if it belongs to this faktur
show_cart = cart and cart_faktur_id == pk
return render(request, 'core/faktur_detail.html', {
'faktur': faktur,
'form': form,
'items': items,
'cart': cart if show_cart else []
})
@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]
cart = request.session.get('export_cart', [])
return render(request, 'core/barang_keluar.html', {
'form': form,
'transactions': transactions,
'cart': cart
})
@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
})
@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
})
# --- IMPORT / EXCEL / OCR ---
@login_required
def download_template(request, type):
if type == 'masuk':
cols = ['SKU', 'Nama Barang', 'Batch', 'Expiry Date (YYYY-MM-DD)', 'Qty', 'Harga Beli', 'Harga Jual']
filename = 'template_barang_masuk.xlsx'
else:
cols = ['SKU', 'Batch Number', 'Qty', 'Catatan']
filename = 'template_barang_keluar.xlsx'
df = pd.DataFrame(columns=cols)
buffer = io.BytesIO()
with pd.ExcelWriter(buffer, engine='openpyxl') as writer:
df.to_excel(writer, index=False)
buffer.seek(0)
response = HttpResponse(buffer.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = f'attachment; filename={filename}'
return response
@login_required
def import_excel(request, type, faktur_id=None):
if request.method == 'POST' and request.FILES.get('file'):
file = request.FILES['file']
try:
df = pd.read_excel(file)
items = []
for _, row in df.iterrows():
if type == 'masuk':
# Validate SKU
sku = str(row.get('SKU', '')).strip()
medicine = Medicine.objects.filter(sku=sku).first()
items.append({
'sku': sku,
'name': row.get('Nama Barang', medicine.name if medicine else 'Unknown'),
'batch': str(row.get('Batch', '')),
'expiry': str(row.get('Expiry Date (YYYY-MM-DD)', '')),
'qty': int(row.get('Qty', 0)),
'buy': float(row.get('Harga Beli', 0)),
'sell': float(row.get('Harga Jual', 0)),
'valid': medicine is not None
})
else:
sku = str(row.get('SKU', '')).strip()
batch_num = str(row.get('Batch Number', '')).strip()
medicine = Medicine.objects.filter(sku=sku).first()
batch = Batch.objects.filter(medicine=medicine, batch_number=batch_num).first() if medicine else None
items.append({
'sku': sku,
'batch': batch_num,
'qty': int(row.get('Qty', 0)),
'note': str(row.get('Catatan', '')),
'valid': batch is not None and batch.quantity >= int(row.get('Qty', 0))
})
if type == 'masuk':
request.session['import_cart'] = items
request.session['cart_faktur_id'] = faktur_id
return redirect('faktur_detail', pk=faktur_id)
else:
request.session['export_cart'] = items
return redirect('barang_keluar')
except Exception as e:
messages.error(request, f"Gagal mengimpor file: {str(e)}")
return redirect('home')
@login_required
def ocr_faktur(request, faktur_id):
if request.method == 'POST' and request.FILES.get('image'):
image_file = request.FILES['image']
try:
# Read image and encode to base64
img_data = base64.b64encode(image_file.read()).decode('utf-8')
prompt = """
Extract items from this invoice image. Return a JSON object with a list 'items'.
Each item should have: 'sku', 'name', 'batch', 'expiry' (YYYY-MM-DD), 'qty', 'buy', 'sell'.
If SKU is not visible, leave it empty.
Format the output strictly as JSON.
"""
response = LocalAIApi.create_response({
"model": "gpt-4o",
"input": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_data}"}}
]
}
],
"text": {"format": {"type": "json_object"}}
})
if response.get("success"):
data = LocalAIApi.decode_json_from_response(response)
items = data.get('items', [])
# Enrich with validation
for item in items:
sku = item.get('sku', '')
medicine = Medicine.objects.filter(sku=sku).first() if sku else None
item['valid'] = medicine is not None
request.session['import_cart'] = items
request.session['cart_faktur_id'] = faktur_id
messages.success(request, "OCR Berhasil. Silakan periksa data di keranjang.")
else:
messages.error(request, f"Gagal OCR: {response.get('error')}")
except Exception as e:
messages.error(request, f"Error OCR: {str(e)}")
return redirect('faktur_detail', pk=faktur_id)
@login_required
def process_cart(request, type, faktur_id=None):
if type == 'masuk':
cart = request.session.get('import_cart', [])
faktur = get_object_or_404(Faktur, pk=faktur_id)
count = 0
for item in cart:
if item.get('valid'):
medicine = Medicine.objects.get(sku=item['sku'])
batch = Batch.objects.create(
medicine=medicine,
faktur=faktur,
batch_number=item['batch'],
expiry_date=item['expiry'],
quantity=item['qty'],
buying_price=item['buy'],
selling_price=item['sell']
)
StockTransaction.objects.create(
medicine=medicine,
batch=batch,
faktur=faktur,
transaction_type='IN',
quantity=item['qty'],
note=f"Import Excel/OCR Faktur {faktur.faktur_number}"
)
count += 1
del request.session['import_cart']
del request.session['cart_faktur_id']
messages.success(request, f"{count} barang berhasil diimpor.")
return redirect('faktur_detail', pk=faktur_id)
else:
cart = request.session.get('export_cart', [])
count = 0
for item in cart:
if item.get('valid'):
medicine = Medicine.objects.get(sku=item['sku'])
batch = Batch.objects.get(medicine=medicine, batch_number=item['batch'])
qty = item['qty']
batch.quantity -= qty
batch.save()
StockTransaction.objects.create(
medicine=medicine,
batch=batch,
transaction_type='OUT',
quantity=qty,
note=item.get('note', 'Import Excel')
)
count += 1
del request.session['export_cart']
messages.success(request, f"{count} barang keluar berhasil diproses.")
return redirect('barang_keluar')
@login_required
def clear_cart(request, type, faktur_id=None):
if type == 'masuk':
if 'import_cart' in request.session:
del request.session['import_cart']
return redirect('faktur_detail', pk=faktur_id)
else:
if 'export_cart' in request.session:
del request.session['export_cart']
return redirect('barang_keluar')