DN V1
This commit is contained in:
parent
28bc475346
commit
04c4db511d
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,11 +1,21 @@
|
||||
from django.contrib import admin
|
||||
from .models import Category, Medicine, Batch, StockTransaction, Supplier, Faktur
|
||||
from .models import Category, Medicine, Batch, StockTransaction, Supplier, Faktur, AppSetting
|
||||
|
||||
# DN-WRS Branding
|
||||
admin.site.site_header = "DN-WRS Admin"
|
||||
admin.site.site_title = "DN-WRS Portal"
|
||||
admin.site.index_title = "Selamat Datang di Manajemen DN-WRS"
|
||||
|
||||
@admin.register(AppSetting)
|
||||
class AppSettingAdmin(admin.ModelAdmin):
|
||||
list_display = ('app_name', 'app_email', 'app_phone')
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Allow only one instance of AppSetting
|
||||
if self.model.objects.exists():
|
||||
return False
|
||||
return True
|
||||
|
||||
@admin.register(Supplier)
|
||||
class SupplierAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'contact_person', 'phone')
|
||||
@ -36,4 +46,4 @@ class BatchAdmin(admin.ModelAdmin):
|
||||
@admin.register(StockTransaction)
|
||||
class StockTransactionAdmin(admin.ModelAdmin):
|
||||
list_display = ('medicine', 'transaction_type', 'quantity', 'created_at')
|
||||
list_filter = ('transaction_type', 'created_at')
|
||||
list_filter = ('transaction_type', 'created_at')
|
||||
|
||||
@ -1,13 +1,23 @@
|
||||
import os
|
||||
import time
|
||||
from .models import AppSetting
|
||||
|
||||
def project_context(request):
|
||||
"""
|
||||
Adds project-specific environment variables to the template context globally.
|
||||
Adds project-specific environment variables and settings to the template context globally.
|
||||
"""
|
||||
settings = AppSetting.objects.first()
|
||||
if not settings:
|
||||
# Create default settings if none exist
|
||||
settings = AppSetting.objects.create(app_name="DN-WRS")
|
||||
|
||||
return {
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_name": settings.app_name,
|
||||
"project_description": settings.app_description or os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_address": settings.app_address,
|
||||
"project_phone": settings.app_phone,
|
||||
"project_email": settings.app_email,
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
# Used for cache-busting static assets
|
||||
"deployment_timestamp": int(time.time()),
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,17 @@
|
||||
from django import forms
|
||||
from .models import Supplier, Faktur, Medicine, Batch, StockTransaction, Category
|
||||
from .models import Supplier, Faktur, Medicine, Batch, StockTransaction, Category, AppSetting
|
||||
|
||||
class AppSettingForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = AppSetting
|
||||
fields = ['app_name', 'app_description', 'app_address', 'app_phone', 'app_email']
|
||||
widgets = {
|
||||
'app_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'app_description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'app_address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'app_phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'app_email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
class CategoryForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@ -104,4 +116,4 @@ class StockOutForm(forms.Form):
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
elif self.initial.get('medicine'):
|
||||
self.fields['batch'].queryset = Batch.objects.filter(medicine_id=self.initial.get('medicine'), quantity__gt=0)
|
||||
self.fields['batch'].queryset = Batch.objects.filter(medicine_id=self.initial.get('medicine'), quantity__gt=0)
|
||||
|
||||
28
core/migrations/0004_appsetting.py
Normal file
28
core/migrations/0004_appsetting.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-06 11:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_medicine_alternative_supplier_medicine_main_supplier'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AppSetting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('app_name', models.CharField(default='DN-WRS', max_length=100, verbose_name='Nama Aplikasi')),
|
||||
('app_description', models.TextField(blank=True, verbose_name='Deskripsi Aplikasi')),
|
||||
('app_address', models.TextField(blank=True, verbose_name='Alamat')),
|
||||
('app_phone', models.CharField(blank=True, max_length=20, verbose_name='Nomor Telepon')),
|
||||
('app_email', models.EmailField(blank=True, max_length=254, verbose_name='Email')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Pengaturan Aplikasi',
|
||||
'verbose_name_plural': 'Pengaturan Aplikasi',
|
||||
},
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0004_appsetting.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0004_appsetting.cpython-311.pyc
Normal file
Binary file not shown.
@ -1,6 +1,20 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
class AppSetting(models.Model):
|
||||
app_name = models.CharField(max_length=100, default="DN-WRS", verbose_name="Nama Aplikasi")
|
||||
app_description = models.TextField(blank=True, verbose_name="Deskripsi Aplikasi")
|
||||
app_address = models.TextField(blank=True, verbose_name="Alamat")
|
||||
app_phone = models.CharField(max_length=20, blank=True, verbose_name="Nomor Telepon")
|
||||
app_email = models.EmailField(blank=True, verbose_name="Email")
|
||||
|
||||
def __str__(self):
|
||||
return self.app_name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Pengaturan Aplikasi"
|
||||
verbose_name_plural = "Pengaturan Aplikasi"
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name="Nama Kategori")
|
||||
description = models.TextField(blank=True, verbose_name="Deskripsi")
|
||||
|
||||
@ -18,110 +18,350 @@
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={% now 'U' %}">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #0d6efd 0%, #00d2ff 100%);
|
||||
--sidebar-width: 280px;
|
||||
--glass-bg: rgba(255, 255, 255, 0.8);
|
||||
--glass-border: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1040;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin-left: var(--sidebar-width);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
padding: 2rem 1.5rem;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-brand span {
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: 800;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex-grow: 1;
|
||||
padding: 0 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-section-title {
|
||||
padding: 1.5rem 1rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: #adb5bd;
|
||||
letter-spacing: 0.05rem;
|
||||
}
|
||||
|
||||
.nav-item-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #495057;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-item-link:hover {
|
||||
background: #f8f9fa;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.nav-item-link.active {
|
||||
background: #e7f1ff;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.nav-item-link i {
|
||||
margin-right: 12px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Top Header in Main Wrapper */
|
||||
.top-header {
|
||||
height: 70px;
|
||||
background: rgba(248, 249, 250, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 2rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container input {
|
||||
padding-left: 40px;
|
||||
border-radius: 50px;
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.search-container i {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.user-profile-btn {
|
||||
background: #fff;
|
||||
border: 1px solid #e9ecef;
|
||||
padding: 5px 15px 5px 5px;
|
||||
border-radius: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-profile-btn:hover {
|
||||
border-color: #0d6efd;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Mobile Adjustments */
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.sidebar.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.main-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1035;
|
||||
}
|
||||
.sidebar-overlay.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% if user.is_authenticated %}
|
||||
<nav class="navbar navbar-expand-lg navbar-custom sticky-top py-3 mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold text-primary d-flex align-items-center" href="{% url 'home' %}">
|
||||
<i data-lucide="package" class="me-2"></i>
|
||||
<span>{{ project_name|default:"DN-WRS" }}</span>
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<a class="sidebar-brand" href="{% url 'home' %}">
|
||||
<i data-lucide="package" class="me-2 text-primary" style="width: 32px; height: 32px;"></i>
|
||||
<span>{{ project_name|default:"DN-WRS" }}</span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-section-title">Menu Utama</div>
|
||||
<a href="{% url 'home' %}" class="nav-item-link {% if request.resolver_match.url_name == 'home' %}active{% endif %}">
|
||||
<i data-lucide="layout-dashboard"></i> Dashboard
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
|
||||
<div class="sidebar-section-title">Manajemen Data</div>
|
||||
<a href="{% url 'medicine_list' %}" class="nav-item-link {% if request.resolver_match.url_name == 'medicine_list' %}active{% endif %}">
|
||||
<i data-lucide="pill"></i> Daftar Barang
|
||||
</a>
|
||||
<a href="{% url 'category_list' %}" class="nav-item-link {% if request.resolver_match.url_name == 'category_list' %}active{% endif %}">
|
||||
<i data-lucide="tag"></i> Kategori
|
||||
</a>
|
||||
<a href="{% url 'supplier_list' %}" class="nav-item-link {% if request.resolver_match.url_name == 'supplier_list' %}active{% endif %}">
|
||||
<i data-lucide="truck"></i> Supplier
|
||||
</a>
|
||||
|
||||
<div class="sidebar-section-title">Transaksi</div>
|
||||
<a href="{% url 'input_faktur' %}" class="nav-item-link {% if request.resolver_match.url_name == 'input_faktur' or request.resolver_match.url_name == 'faktur_detail' %}active{% endif %}">
|
||||
<i data-lucide="file-plus"></i> Inbound / Faktur
|
||||
</a>
|
||||
<a href="{% url 'barang_keluar' %}" class="nav-item-link {% if request.resolver_match.url_name == 'barang_keluar' %}active{% endif %}">
|
||||
<i data-lucide="file-minus"></i> Outbound / Keluar
|
||||
</a>
|
||||
<a href="{% url 'laporan_transaksi' %}" class="nav-item-link {% if request.resolver_match.url_name == 'laporan_transaksi' %}active{% endif %}">
|
||||
<i data-lucide="bar-chart-2"></i> Laporan Stok
|
||||
</a>
|
||||
|
||||
<div class="sidebar-section-title">Sistem</div>
|
||||
<a href="{% url 'app_settings' %}" class="nav-item-link {% if request.resolver_match.url_name == 'app_settings' %}active{% endif %}">
|
||||
<i data-lucide="settings"></i> Pengaturan Aplikasi
|
||||
</a>
|
||||
<a href="/admin/password_change/" class="nav-item-link">
|
||||
<i data-lucide="key"></i> Keamanan
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="p-3 border-top">
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="nav-item-link text-danger border-0 bg-transparent w-100 text-start">
|
||||
<i data-lucide="log-out"></i> Keluar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<!-- Top Header -->
|
||||
<header class="top-header">
|
||||
<button class="btn btn-link p-0 me-3 d-lg-none text-dark shadow-none" id="sidebarToggle">
|
||||
<i data-lucide="menu"></i>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'home' %}active{% endif %}" href="{% url 'home' %}">
|
||||
<i data-lucide="layout-dashboard" class="me-2 icon-sm"></i> Beranda
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link px-3 dropdown-toggle d-flex align-items-center" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i data-lucide="database" class="me-2 icon-sm"></i> Master Data
|
||||
</a>
|
||||
<ul class="dropdown-menu border-0 shadow-lg rounded-4 p-2">
|
||||
<li><a class="dropdown-item rounded-3" href="{% url 'medicine_list' %}"><i data-lucide="pill" class="me-2 icon-sm"></i> Data Barang</a></li>
|
||||
<li><a class="dropdown-item rounded-3" href="{% url 'category_list' %}"><i data-lucide="tag" class="me-2 icon-sm"></i> Kategori</a></li>
|
||||
<li><a class="dropdown-item rounded-3" href="{% url 'supplier_list' %}"><i data-lucide="truck" class="me-2 icon-sm"></i> Supplier</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'input_faktur' %}active{% endif %}" href="{% url 'input_faktur' %}">
|
||||
<i data-lucide="file-plus" class="me-2 icon-sm"></i> Faktur Masuk
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'barang_keluar' %}active{% endif %}" href="{% url 'barang_keluar' %}">
|
||||
<i data-lucide="file-minus" class="me-2 icon-sm"></i> Barang Keluar
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'laporan_transaksi' %}active{% endif %}" href="{% url 'laporan_transaksi' %}">
|
||||
<i data-lucide="bar-chart-2" class="me-2 icon-sm"></i> Laporan
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link text-dark text-decoration-none dropdown-toggle d-flex align-items-center" type="button" id="userDropdown" data-bs-toggle="dropdown">
|
||||
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-size: 0.8rem;">
|
||||
{{ user.username|slice:":1"|upper }}
|
||||
</div>
|
||||
<span class="fw-semibold">{{ user.username }}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-lg rounded-4 p-2">
|
||||
<li><a class="dropdown-item rounded-3 py-2" href="/admin/password_change/">Ubah Password</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item rounded-3 py-2 text-danger">Keluar</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form action="{% url 'medicine_list' %}" method="get" class="search-container d-none d-md-block">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" name="q" class="form-control border-0 shadow-sm" placeholder="Cari barang atau faktur..." value="{{ query|default:'' }}">
|
||||
</form>
|
||||
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
<div class="dropdown">
|
||||
<button class="user-profile-btn shadow-none border-0 d-flex align-items-center" type="button" data-bs-toggle="dropdown">
|
||||
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-weight: 700; font-size: 0.8rem;">
|
||||
{{ user.username|slice:":1"|upper }}
|
||||
</div>
|
||||
<span class="small fw-bold d-none d-sm-block">{{ user.username }}</span>
|
||||
<i data-lucide="chevron-down" class="ms-1 text-muted" style="width: 14px; height: 14px;"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-lg rounded-4 p-2 mt-2">
|
||||
<li><a class="dropdown-item rounded-3 py-2" href="{% url 'app_settings' %}"><i data-lucide="settings" class="me-2 icon-sm"></i> Pengaturan</a></li>
|
||||
<li><hr class="dropdown-divider opacity-50"></li>
|
||||
<li>
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item rounded-3 py-2 text-danger"><i data-lucide="log-out" class="me-2 icon-sm"></i> Keluar</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container-fluid px-4 py-4">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-4 border-0 shadow-sm mb-4" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
{% if message.tags == 'success' %}
|
||||
<i data-lucide="check-circle" class="me-2 text-success"></i>
|
||||
{% elif message.tags == 'error' %}
|
||||
<i data-lucide="x-circle" class="me-2 text-danger"></i>
|
||||
{% else %}
|
||||
<i data-lucide="alert-circle" class="me-2 text-info"></i>
|
||||
{% endif %}
|
||||
<span class="fw-medium">{{ message }}</span>
|
||||
</div>
|
||||
<button type="button" class="btn-close shadow-none" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<main class="fade-in">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="mt-auto py-4 bg-white border-top">
|
||||
<div class="container-fluid px-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6 text-center text-md-start">
|
||||
<p class="mb-0 text-muted small">© {% now "Y" %} {{ project_name|default:"DN-WRS" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<div class="d-flex justify-content-center justify-content-md-end gap-3 text-muted" style="font-size: 0.75rem;">
|
||||
<span>v2.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div class="container">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-4 border-0 shadow-sm mb-4" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i data-lucide="{% if message.tags == 'success' %}check-circle{% else %}alert-circle{% endif %}" class="me-2"></i>
|
||||
{{ message }}
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<main class="container py-2 fade-in">
|
||||
{% block content %}{% endblock %}
|
||||
{% else %}
|
||||
<main>
|
||||
{% block login_content %}{% block content_no_auth %}{% endblock %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="container py-5 text-center text-muted small mt-auto">
|
||||
<p class="mb-0">© {% now "Y" %} {{ project_name|default:"DN-WRS" }}</p>
|
||||
<p class="opacity-50">Sistem Manajemen Gudang Premium</p>
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// Custom icon sizes
|
||||
document.querySelectorAll('.icon-sm').forEach(el => {
|
||||
el.setAttribute('width', '18');
|
||||
el.setAttribute('height', '18');
|
||||
});
|
||||
// Sidebar Toggle for Mobile
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
const toggle = document.getElementById('sidebarToggle');
|
||||
|
||||
if (toggle) {
|
||||
toggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('show');
|
||||
overlay.classList.toggle('show');
|
||||
});
|
||||
}
|
||||
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', () => {
|
||||
sidebar.classList.remove('show');
|
||||
overlay.classList.remove('show');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@ -1,8 +1,56 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
{% if cart %}
|
||||
<div class="card glass-card border-0 shadow-sm mb-4 border-start border-4 border-warning">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold mb-0 text-warning d-flex align-items-center">
|
||||
<i data-lucide="shopping-cart" class="me-2"></i> Konfirmasi Import Barang Keluar
|
||||
</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'clear_cart' 'keluar' %}" class="btn btn-light btn-sm rounded-pill px-3 text-danger">Batalkan Semua</a>
|
||||
<a href="{% url 'process_cart' 'keluar' %}" class="btn btn-warning btn-sm rounded-pill px-4 fw-bold">Proses Pengeluaran Barang</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Barang (SKU)</th>
|
||||
<th>Batch Number</th>
|
||||
<th class="text-end">Qty Keluar</th>
|
||||
<th>Catatan</th>
|
||||
<th class="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in cart %}
|
||||
<tr class="{% if not item.valid %}table-danger-subtle{% endif %}">
|
||||
<td>
|
||||
<div class="fw-bold">{{ item.sku }}</div>
|
||||
</td>
|
||||
<td><code>{{ item.batch }}</code></td>
|
||||
<td class="text-end">{{ item.qty }}</td>
|
||||
<td>{{ item.note }}</td>
|
||||
<td class="text-center">
|
||||
{% if item.valid %}
|
||||
<span class="badge bg-success rounded-pill"><i data-lucide="check" class="icon-xs"></i> Ready</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger rounded-pill">Batch/Stok Tidak Valid</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<div class="card glass-card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="fw-bold mb-4 d-flex align-items-center">
|
||||
<div class="bg-danger-subtle text-danger p-2 rounded-3 me-3">
|
||||
@ -35,6 +83,29 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card glass-card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold mb-0 d-flex align-items-center">
|
||||
<div class="bg-info-subtle text-info p-2 rounded-3 me-3">
|
||||
<i data-lucide="file-spreadsheet"></i>
|
||||
</div>
|
||||
Import Excel
|
||||
</h5>
|
||||
<a href="{% url 'download_template' 'keluar' %}" class="btn btn-sm btn-outline-success rounded-pill">Template</a>
|
||||
</div>
|
||||
<form action="{% url 'import_excel' 'keluar' %}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<input type="file" name="file" class="form-control" accept=".xlsx, .xls" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-info text-white w-100 shadow-sm">
|
||||
<i data-lucide="upload" class="me-2 icon-sm"></i> Upload Excel Keluar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
|
||||
@ -13,35 +13,89 @@
|
||||
<h6 class="text-muted small fw-bold text-uppercase mb-1" style="letter-spacing: 1px;">NOMOR FAKTUR</h6>
|
||||
<h2 class="fw-bold mb-0 text-primary">#{{ faktur.faktur_number }}</h2>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<h6 class="text-muted small fw-bold text-uppercase mb-1" style="letter-spacing: 1px;">SUPPLIER</h6>
|
||||
<div class="badge bg-primary-subtle text-primary rounded-pill px-4 py-2 fs-6 fw-bold">
|
||||
<i data-lucide="truck" class="me-2 icon-sm"></i> {{ faktur.supplier.name|default:"Internal" }}
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'download_template' 'masuk' %}" class="btn btn-outline-success rounded-pill px-3">
|
||||
<i data-lucide="download" class="me-1 icon-sm"></i> Template Excel
|
||||
</a>
|
||||
<div class="text-end ms-4">
|
||||
<h6 class="text-muted small fw-bold text-uppercase mb-1" style="letter-spacing: 1px;">SUPPLIER</h6>
|
||||
<div class="badge bg-primary-subtle text-primary rounded-pill px-4 py-2 fs-6 fw-bold">
|
||||
<i data-lucide="truck" class="me-2 icon-sm"></i> {{ faktur.supplier.name|default:"Internal" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if cart %}
|
||||
<div class="card glass-card border-0 shadow-sm mb-4 border-start border-4 border-warning">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold mb-0 text-warning d-flex align-items-center">
|
||||
<i data-lucide="shopping-cart" class="me-2"></i> Konfirmasi Import (Keranjang)
|
||||
</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'clear_cart_faktur' 'masuk' faktur.id %}" class="btn btn-light btn-sm rounded-pill px-3 text-danger">Batalkan Semua</a>
|
||||
<a href="{% url 'process_cart_faktur' 'masuk' faktur.id %}" class="btn btn-warning btn-sm rounded-pill px-4 fw-bold">Proses & Simpan Ke Database</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Barang (SKU)</th>
|
||||
<th>Batch</th>
|
||||
<th>Expired</th>
|
||||
<th class="text-end">Qty</th>
|
||||
<th class="text-end">Harga Beli</th>
|
||||
<th class="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in cart %}
|
||||
<tr class="{% if not item.valid %}table-danger-subtle{% endif %}">
|
||||
<td>
|
||||
<div class="fw-bold">{{ item.name }}</div>
|
||||
<div class="small text-muted">{{ item.sku }}</div>
|
||||
</td>
|
||||
<td><code>{{ item.batch }}</code></td>
|
||||
<td>{{ item.expiry }}</td>
|
||||
<td class="text-end">{{ item.qty }}</td>
|
||||
<td class="text-end">Rp {{ item.buy|floatformat:2 }}</td>
|
||||
<td class="text-center">
|
||||
{% if item.valid %}
|
||||
<span class="badge bg-success rounded-pill"><i data-lucide="check" class="icon-xs"></i> Ready</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger rounded-pill" title="SKU tidak ditemukan di database Master Barang">SKU Tidak Dikenal</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<div class="card glass-card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold mb-4 d-flex align-items-center">
|
||||
<div class="bg-success-subtle text-success p-2 rounded-3 me-3">
|
||||
<i data-lucide="package-plus"></i>
|
||||
</div>
|
||||
Input Barang
|
||||
Input Manual
|
||||
</h5>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="add_item" value="1">
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<div class="text-danger small">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary w-100 py-3 mt-3 shadow-sm d-flex align-items-center justify-content-center">
|
||||
@ -50,6 +104,46 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card glass-card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold mb-3 d-flex align-items-center">
|
||||
<div class="bg-info-subtle text-info p-2 rounded-3 me-3">
|
||||
<i data-lucide="file-spreadsheet"></i>
|
||||
</div>
|
||||
Import Excel
|
||||
</h5>
|
||||
<form action="{% url 'import_excel_faktur' 'masuk' faktur.id %}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<input type="file" name="file" class="form-control" accept=".xlsx, .xls" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-info text-white w-100 shadow-sm">
|
||||
<i data-lucide="upload" class="me-2 icon-sm"></i> Upload Excel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold mb-3 d-flex align-items-center">
|
||||
<div class="bg-purple-subtle text-purple p-2 rounded-3 me-3">
|
||||
<i data-lucide="scan"></i>
|
||||
</div>
|
||||
Scan Foto Faktur
|
||||
</h5>
|
||||
<form action="{% url 'ocr_faktur' faktur.id %}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<input type="file" name="image" class="form-control" accept="image/*" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-purple text-white w-100 shadow-sm">
|
||||
<i data-lucide="camera" class="me-2 icon-sm"></i> Scan & Ekstrak Data
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
@ -59,9 +153,9 @@
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="list"></i>
|
||||
</div>
|
||||
Rincian Barang
|
||||
Rincian Barang Terinput
|
||||
</h5>
|
||||
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ items|length }} Item</span>
|
||||
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ items|length }} Item Terdaftar</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
@ -106,6 +200,11 @@
|
||||
<!-- Select2 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
.bg-purple-subtle { background-color: #f3e8ff; }
|
||||
.text-purple { color: #7e22ce; }
|
||||
.btn-purple { background-color: #9333ea; border-color: #9333ea; }
|
||||
.btn-purple:hover { background-color: #7e22ce; border-color: #7e22ce; }
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 48px;
|
||||
|
||||
85
core/templates/core/settings.html
Normal file
85
core/templates/core/settings.html
Normal file
@ -0,0 +1,85 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-lg rounded-4 overflow-hidden mb-4">
|
||||
<div class="card-header bg-white border-0 py-4 px-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary bg-opacity-10 p-3 rounded-4 me-3">
|
||||
<i data-lucide="settings" class="text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="fw-bold mb-0">Pengaturan Aplikasi</h4>
|
||||
<p class="text-muted small mb-0">Konfigurasi identitas sistem pergudangan</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-4">
|
||||
<label class="form-label fw-semibold">Nama Aplikasi</label>
|
||||
{{ form.app_name }}
|
||||
<div class="form-text">Nama ini akan muncul di navigasi dan judul halaman.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-4">
|
||||
<label class="form-label fw-semibold">Deskripsi</label>
|
||||
{{ form.app_description }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<label class="form-label fw-semibold">Nomor Telepon</label>
|
||||
{{ form.app_phone }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<label class="form-label fw-semibold">Email</label>
|
||||
{{ form.app_email }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-4">
|
||||
<label class="form-label fw-semibold">Alamat</label>
|
||||
{{ form.app_address }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mt-2">
|
||||
<a href="{% url 'home' %}" class="btn btn-light px-4 rounded-3">Batal</a>
|
||||
<button type="submit" class="btn btn-primary px-4 rounded-3 d-flex align-items-center">
|
||||
<i data-lucide="save" class="me-2 icon-sm"></i> Simpan Perubahan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<i data-lucide="info" class="text-info me-2"></i>
|
||||
<h5 class="fw-bold mb-0">Informasi Sistem</h5>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 bg-light rounded-3">
|
||||
<div class="text-muted small">Django Version</div>
|
||||
<div class="fw-bold">{{ django_version }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 bg-light rounded-3">
|
||||
<div class="text-muted small">Python Version</div>
|
||||
<div class="fw-bold">{{ python_version }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
core/urls.py
11
core/urls.py
@ -3,6 +3,7 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
path('settings/', views.app_settings, name='app_settings'),
|
||||
path('medicines/', views.medicine_list, name='medicine_list'),
|
||||
path('medicines/export-low-stock/', views.export_low_stock_pdf, name='export_low_stock_pdf'),
|
||||
path('suppliers/', views.supplier_list, name='supplier_list'),
|
||||
@ -14,4 +15,14 @@ urlpatterns = [
|
||||
path('laporan/', views.laporan_transaksi, name='laporan_transaksi'),
|
||||
path('transaksi/<int:pk>/delete/', views.delete_transaksi, name='delete_transaksi'),
|
||||
path('transaksi/<int:pk>/edit/', views.edit_transaksi, name='edit_transaksi'),
|
||||
|
||||
# Import / Template / OCR
|
||||
path('template/download/<str:type>/', views.download_template, name='download_template'),
|
||||
path('import/excel/<str:type>/', views.import_excel, name='import_excel'),
|
||||
path('import/excel/<str:type>/<int:faktur_id>/', views.import_excel, name='import_excel_faktur'),
|
||||
path('ocr/faktur/<int:faktur_id>/', views.ocr_faktur, name='ocr_faktur'),
|
||||
path('cart/process/<str:type>/', views.process_cart, name='process_cart'),
|
||||
path('cart/process/<str:type>/<int:faktur_id>/', views.process_cart, name='process_cart_faktur'),
|
||||
path('cart/clear/<str:type>/', views.clear_cart, name='clear_cart'),
|
||||
path('cart/clear/<str:type>/<int:faktur_id>/', views.clear_cart, name='clear_cart_faktur'),
|
||||
]
|
||||
|
||||
300
core/views.py
300
core/views.py
@ -1,7 +1,10 @@
|
||||
import os
|
||||
import platform
|
||||
import io
|
||||
from datetime import timedelta
|
||||
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
|
||||
@ -9,20 +12,25 @@ 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 .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()
|
||||
|
||||
@ -71,7 +79,7 @@ def home(request):
|
||||
chart_data_out.append(out_sum)
|
||||
|
||||
context = {
|
||||
"project_name": "DN-WRS",
|
||||
"project_name": project_name,
|
||||
"total_medicines": total_medicines,
|
||||
"total_stock": total_stock,
|
||||
"expired_count": expired_batches_count,
|
||||
@ -87,6 +95,23 @@ def home(request):
|
||||
}
|
||||
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
|
||||
@ -103,8 +128,7 @@ def category_list(request):
|
||||
categories = Category.objects.all().order_by('name')
|
||||
return render(request, 'core/categories.html', {
|
||||
'categories': categories,
|
||||
'form': form,
|
||||
'project_name': 'DN-WRS'
|
||||
'form': form
|
||||
})
|
||||
|
||||
@login_required
|
||||
@ -121,8 +145,7 @@ def supplier_list(request):
|
||||
suppliers = Supplier.objects.all().order_by('name')
|
||||
return render(request, 'core/suppliers.html', {
|
||||
'suppliers': suppliers,
|
||||
'form': form,
|
||||
'project_name': 'DN-WRS'
|
||||
'form': form
|
||||
})
|
||||
|
||||
@login_required
|
||||
@ -164,7 +187,6 @@ def medicine_list(request):
|
||||
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,
|
||||
@ -261,46 +283,52 @@ def input_faktur(request):
|
||||
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'
|
||||
'fakturs': fakturs
|
||||
})
|
||||
|
||||
@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)
|
||||
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,
|
||||
'project_name': 'DN-WRS'
|
||||
'cart': cart if show_cart else []
|
||||
})
|
||||
|
||||
@login_required
|
||||
@ -333,10 +361,12 @@ def barang_keluar(request):
|
||||
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,
|
||||
'project_name': 'DN-WRS'
|
||||
'cart': cart
|
||||
})
|
||||
|
||||
@login_required
|
||||
@ -349,8 +379,7 @@ def get_batches(request):
|
||||
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'
|
||||
'transactions': transactions
|
||||
})
|
||||
|
||||
@login_required
|
||||
@ -398,6 +427,195 @@ def edit_transaksi(request, pk):
|
||||
return redirect('laporan_transaksi')
|
||||
|
||||
return render(request, 'core/edit_transaksi.html', {
|
||||
'transaction': transaction,
|
||||
'project_name': 'DN-WRS'
|
||||
})
|
||||
'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')
|
||||
@ -1,4 +1,8 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
reportlab==4.1.0
|
||||
reportlab==4.1.0
|
||||
pandas==2.2.3
|
||||
openpyxl==3.1.5
|
||||
Pillow==11.1.0
|
||||
httpx==0.28.1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user