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 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
|
# DN-WRS Branding
|
||||||
admin.site.site_header = "DN-WRS Admin"
|
admin.site.site_header = "DN-WRS Admin"
|
||||||
admin.site.site_title = "DN-WRS Portal"
|
admin.site.site_title = "DN-WRS Portal"
|
||||||
admin.site.index_title = "Selamat Datang di Manajemen DN-WRS"
|
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)
|
@admin.register(Supplier)
|
||||||
class SupplierAdmin(admin.ModelAdmin):
|
class SupplierAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'contact_person', 'phone')
|
list_display = ('name', 'contact_person', 'phone')
|
||||||
@ -36,4 +46,4 @@ class BatchAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(StockTransaction)
|
@admin.register(StockTransaction)
|
||||||
class StockTransactionAdmin(admin.ModelAdmin):
|
class StockTransactionAdmin(admin.ModelAdmin):
|
||||||
list_display = ('medicine', 'transaction_type', 'quantity', 'created_at')
|
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 os
|
||||||
import time
|
import time
|
||||||
|
from .models import AppSetting
|
||||||
|
|
||||||
def project_context(request):
|
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 {
|
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", ""),
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
# Used for cache-busting static assets
|
# Used for cache-busting static assets
|
||||||
"deployment_timestamp": int(time.time()),
|
"deployment_timestamp": int(time.time()),
|
||||||
}
|
}
|
||||||
@ -1,5 +1,17 @@
|
|||||||
from django import forms
|
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 CategoryForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -104,4 +116,4 @@ class StockOutForm(forms.Form):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
elif self.initial.get('medicine'):
|
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.db import models
|
||||||
from django.utils import timezone
|
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):
|
class Category(models.Model):
|
||||||
name = models.CharField(max_length=100, verbose_name="Nama Kategori")
|
name = models.CharField(max_length=100, verbose_name="Nama Kategori")
|
||||||
description = models.TextField(blank=True, verbose_name="Deskripsi")
|
description = models.TextField(blank=True, verbose_name="Deskripsi")
|
||||||
|
|||||||
@ -18,110 +18,350 @@
|
|||||||
<!-- Custom CSS -->
|
<!-- Custom CSS -->
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={% now 'U' %}">
|
<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 %}
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<nav class="navbar navbar-expand-lg navbar-custom sticky-top py-3 mb-4">
|
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand fw-bold text-primary d-flex align-items-center" href="{% url 'home' %}">
|
<!-- Sidebar -->
|
||||||
<i data-lucide="package" class="me-2"></i>
|
<aside class="sidebar" id="sidebar">
|
||||||
<span>{{ project_name|default:"DN-WRS" }}</span>
|
<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>
|
</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>
|
<i data-lucide="menu"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
|
||||||
<ul class="navbar-nav me-auto">
|
<form action="{% url 'medicine_list' %}" method="get" class="search-container d-none d-md-block">
|
||||||
<li class="nav-item">
|
<i data-lucide="search"></i>
|
||||||
<a class="nav-link px-3 d-flex align-items-center {% if request.resolver_match.url_name == 'home' %}active{% endif %}" href="{% url 'home' %}">
|
<input type="text" name="q" class="form-control border-0 shadow-sm" placeholder="Cari barang atau faktur..." value="{{ query|default:'' }}">
|
||||||
<i data-lucide="layout-dashboard" class="me-2 icon-sm"></i> Beranda
|
</form>
|
||||||
</a>
|
|
||||||
</li>
|
<div class="ms-auto d-flex align-items-center">
|
||||||
<li class="nav-item dropdown">
|
<div class="dropdown">
|
||||||
<a class="nav-link px-3 dropdown-toggle d-flex align-items-center" href="#" role="button" data-bs-toggle="dropdown">
|
<button class="user-profile-btn shadow-none border-0 d-flex align-items-center" type="button" data-bs-toggle="dropdown">
|
||||||
<i data-lucide="database" class="me-2 icon-sm"></i> Master Data
|
<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;">
|
||||||
</a>
|
{{ user.username|slice:":1"|upper }}
|
||||||
<ul class="dropdown-menu border-0 shadow-lg rounded-4 p-2">
|
</div>
|
||||||
<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>
|
<span class="small fw-bold d-none d-sm-block">{{ user.username }}</span>
|
||||||
<li><a class="dropdown-item rounded-3" href="{% url 'category_list' %}"><i data-lucide="tag" class="me-2 icon-sm"></i> Kategori</a></li>
|
<i data-lucide="chevron-down" class="ms-1 text-muted" style="width: 14px; height: 14px;"></i>
|
||||||
<li><a class="dropdown-item rounded-3" href="{% url 'supplier_list' %}"><i data-lucide="truck" class="me-2 icon-sm"></i> Supplier</a></li>
|
</button>
|
||||||
</ul>
|
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-lg rounded-4 p-2 mt-2">
|
||||||
</li>
|
<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 class="nav-item">
|
<li><hr class="dropdown-divider opacity-50"></li>
|
||||||
<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' %}">
|
<li>
|
||||||
<i data-lucide="file-plus" class="me-2 icon-sm"></i> Faktur Masuk
|
<form method="post" action="{% url 'logout' %}">
|
||||||
</a>
|
{% csrf_token %}
|
||||||
</li>
|
<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>
|
||||||
<li class="nav-item">
|
</form>
|
||||||
<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' %}">
|
</li>
|
||||||
<i data-lucide="file-minus" class="me-2 icon-sm"></i> Barang Keluar
|
</ul>
|
||||||
</a>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<li class="nav-item">
|
</header>
|
||||||
<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
|
<div class="container-fluid px-4 py-4">
|
||||||
</a>
|
{% if messages %}
|
||||||
</li>
|
{% for message in messages %}
|
||||||
</ul>
|
<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">
|
<div class="d-flex align-items-center">
|
||||||
<div class="dropdown">
|
{% if message.tags == 'success' %}
|
||||||
<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">
|
<i data-lucide="check-circle" class="me-2 text-success"></i>
|
||||||
<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;">
|
{% elif message.tags == 'error' %}
|
||||||
{{ user.username|slice:":1"|upper }}
|
<i data-lucide="x-circle" class="me-2 text-danger"></i>
|
||||||
</div>
|
{% else %}
|
||||||
<span class="fw-semibold">{{ user.username }}</span>
|
<i data-lucide="alert-circle" class="me-2 text-info"></i>
|
||||||
</button>
|
{% endif %}
|
||||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-lg rounded-4 p-2">
|
<span class="fw-medium">{{ message }}</span>
|
||||||
<li><a class="dropdown-item rounded-3 py-2" href="/admin/password_change/">Ubah Password</a></li>
|
</div>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<button type="button" class="btn-close shadow-none" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
<li>
|
</div>
|
||||||
<form method="post" action="{% url 'logout' %}">
|
{% endfor %}
|
||||||
{% csrf_token %}
|
{% endif %}
|
||||||
<button type="submit" class="dropdown-item rounded-3 py-2 text-danger">Keluar</button>
|
|
||||||
</form>
|
<main class="fade-in">
|
||||||
</li>
|
{% block content %}{% endblock %}
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
</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 %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
<main class="container py-2 fade-in">
|
<main>
|
||||||
{% block content %}{% endblock %}
|
{% block login_content %}{% block content_no_auth %}{% endblock %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
{% endif %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Initialize Lucide icons
|
// Initialize Lucide icons
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
|
||||||
// Custom icon sizes
|
// Sidebar Toggle for Mobile
|
||||||
document.querySelectorAll('.icon-sm').forEach(el => {
|
const sidebar = document.getElementById('sidebar');
|
||||||
el.setAttribute('width', '18');
|
const overlay = document.getElementById('sidebarOverlay');
|
||||||
el.setAttribute('height', '18');
|
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>
|
</script>
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,8 +1,56 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block content %}
|
{% 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="row g-4">
|
||||||
<div class="col-md-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">
|
<div class="card-body p-4">
|
||||||
<h4 class="fw-bold mb-4 d-flex align-items-center">
|
<h4 class="fw-bold mb-4 d-flex align-items-center">
|
||||||
<div class="bg-danger-subtle text-danger p-2 rounded-3 me-3">
|
<div class="bg-danger-subtle text-danger p-2 rounded-3 me-3">
|
||||||
@ -35,6 +83,29 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card glass-card border-0 shadow-sm h-100">
|
<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>
|
<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>
|
<h2 class="fw-bold mb-0 text-primary">#{{ faktur.faktur_number }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="d-flex gap-2">
|
||||||
<h6 class="text-muted small fw-bold text-uppercase mb-1" style="letter-spacing: 1px;">SUPPLIER</h6>
|
<a href="{% url 'download_template' 'masuk' %}" class="btn btn-outline-success rounded-pill px-3">
|
||||||
<div class="badge bg-primary-subtle text-primary rounded-pill px-4 py-2 fs-6 fw-bold">
|
<i data-lucide="download" class="me-1 icon-sm"></i> Template Excel
|
||||||
<i data-lucide="truck" class="me-2 icon-sm"></i> {{ faktur.supplier.name|default:"Internal" }}
|
</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>
|
</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="row g-4">
|
||||||
<div class="col-md-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">
|
<div class="card-body p-4">
|
||||||
<h5 class="fw-bold mb-4 d-flex align-items-center">
|
<h5 class="fw-bold mb-4 d-flex align-items-center">
|
||||||
<div class="bg-success-subtle text-success p-2 rounded-3 me-3">
|
<div class="bg-success-subtle text-success p-2 rounded-3 me-3">
|
||||||
<i data-lucide="package-plus"></i>
|
<i data-lucide="package-plus"></i>
|
||||||
</div>
|
</div>
|
||||||
Input Barang
|
Input Manual
|
||||||
</h5>
|
</h5>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="add_item" value="1">
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
<div class="mb-3">
|
<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>
|
<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 }}
|
{{ field }}
|
||||||
{% if field.errors %}
|
|
||||||
<div class="text-danger small">{{ field.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button type="submit" class="btn btn-primary w-100 py-3 mt-3 shadow-sm d-flex align-items-center justify-content-center">
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card glass-card border-0 shadow-sm h-100">
|
<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">
|
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||||
<i data-lucide="list"></i>
|
<i data-lucide="list"></i>
|
||||||
</div>
|
</div>
|
||||||
Rincian Barang
|
Rincian Barang Terinput
|
||||||
</h5>
|
</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>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table align-middle">
|
<table class="table align-middle">
|
||||||
@ -106,6 +200,11 @@
|
|||||||
<!-- Select2 CSS -->
|
<!-- Select2 CSS -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
<style>
|
<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 {
|
.select2-container--default .select2-selection--single {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
height: 48px;
|
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 = [
|
urlpatterns = [
|
||||||
path('', views.home, name='home'),
|
path('', views.home, name='home'),
|
||||||
|
path('settings/', views.app_settings, name='app_settings'),
|
||||||
path('medicines/', views.medicine_list, name='medicine_list'),
|
path('medicines/', views.medicine_list, name='medicine_list'),
|
||||||
path('medicines/export-low-stock/', views.export_low_stock_pdf, name='export_low_stock_pdf'),
|
path('medicines/export-low-stock/', views.export_low_stock_pdf, name='export_low_stock_pdf'),
|
||||||
path('suppliers/', views.supplier_list, name='supplier_list'),
|
path('suppliers/', views.supplier_list, name='supplier_list'),
|
||||||
@ -14,4 +15,14 @@ urlpatterns = [
|
|||||||
path('laporan/', views.laporan_transaksi, name='laporan_transaksi'),
|
path('laporan/', views.laporan_transaksi, name='laporan_transaksi'),
|
||||||
path('transaksi/<int:pk>/delete/', views.delete_transaksi, name='delete_transaksi'),
|
path('transaksi/<int:pk>/delete/', views.delete_transaksi, name='delete_transaksi'),
|
||||||
path('transaksi/<int:pk>/edit/', views.edit_transaksi, name='edit_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 os
|
||||||
import platform
|
import platform
|
||||||
import io
|
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 import get_version as django_version
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.utils import timezone
|
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.auth.decorators import login_required
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import JsonResponse, HttpResponse
|
from django.http import JsonResponse, HttpResponse
|
||||||
from .models import Medicine, Batch, Category, StockTransaction, Supplier, Faktur
|
from .models import Medicine, Batch, Category, StockTransaction, Supplier, Faktur, AppSetting
|
||||||
from .forms import SupplierForm, FakturForm, StockInForm, StockOutForm, CategoryForm, MedicineForm
|
from .forms import SupplierForm, FakturForm, StockInForm, StockOutForm, CategoryForm, MedicineForm, AppSettingForm
|
||||||
|
|
||||||
from reportlab.lib import colors
|
from reportlab.lib import colors
|
||||||
from reportlab.lib.pagesizes import letter, A4
|
from reportlab.lib.pagesizes import letter, A4
|
||||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
|
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
|
||||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
|
||||||
|
from ai.local_ai_api import LocalAIApi
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the medicine warehouse dashboard."""
|
"""Render the medicine warehouse dashboard."""
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
today = now.date()
|
today = now.date()
|
||||||
|
|
||||||
|
settings = AppSetting.objects.first()
|
||||||
|
project_name = settings.app_name if settings else "DN-WRS"
|
||||||
|
|
||||||
# Stats
|
# Stats
|
||||||
total_medicines = Medicine.objects.count()
|
total_medicines = Medicine.objects.count()
|
||||||
|
|
||||||
@ -71,7 +79,7 @@ def home(request):
|
|||||||
chart_data_out.append(out_sum)
|
chart_data_out.append(out_sum)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"project_name": "DN-WRS",
|
"project_name": project_name,
|
||||||
"total_medicines": total_medicines,
|
"total_medicines": total_medicines,
|
||||||
"total_stock": total_stock,
|
"total_stock": total_stock,
|
||||||
"expired_count": expired_batches_count,
|
"expired_count": expired_batches_count,
|
||||||
@ -87,6 +95,23 @@ def home(request):
|
|||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
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 ---
|
# --- MASTER DATA MANAGEMENT ---
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -103,8 +128,7 @@ def category_list(request):
|
|||||||
categories = Category.objects.all().order_by('name')
|
categories = Category.objects.all().order_by('name')
|
||||||
return render(request, 'core/categories.html', {
|
return render(request, 'core/categories.html', {
|
||||||
'categories': categories,
|
'categories': categories,
|
||||||
'form': form,
|
'form': form
|
||||||
'project_name': 'DN-WRS'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -121,8 +145,7 @@ def supplier_list(request):
|
|||||||
suppliers = Supplier.objects.all().order_by('name')
|
suppliers = Supplier.objects.all().order_by('name')
|
||||||
return render(request, 'core/suppliers.html', {
|
return render(request, 'core/suppliers.html', {
|
||||||
'suppliers': suppliers,
|
'suppliers': suppliers,
|
||||||
'form': form,
|
'form': form
|
||||||
'project_name': 'DN-WRS'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -164,7 +187,6 @@ def medicine_list(request):
|
|||||||
return render(request, 'core/medicines.html', {
|
return render(request, 'core/medicines.html', {
|
||||||
'medicines': medicines,
|
'medicines': medicines,
|
||||||
'form': form,
|
'form': form,
|
||||||
'project_name': 'DN-WRS',
|
|
||||||
'query': query,
|
'query': query,
|
||||||
'show_low_stock': show_low_stock,
|
'show_low_stock': show_low_stock,
|
||||||
'low_stock_count': low_stock_count,
|
'low_stock_count': low_stock_count,
|
||||||
@ -261,46 +283,52 @@ def input_faktur(request):
|
|||||||
fakturs = Faktur.objects.all().order_by('-created_at')
|
fakturs = Faktur.objects.all().order_by('-created_at')
|
||||||
return render(request, 'core/input_faktur.html', {
|
return render(request, 'core/input_faktur.html', {
|
||||||
'faktur_form': faktur_form,
|
'faktur_form': faktur_form,
|
||||||
'fakturs': fakturs,
|
'fakturs': fakturs
|
||||||
'project_name': 'DN-WRS'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def faktur_detail(request, pk):
|
def faktur_detail(request, pk):
|
||||||
faktur = get_object_or_404(Faktur, pk=pk)
|
faktur = get_object_or_404(Faktur, pk=pk)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = StockInForm(request.POST)
|
if 'add_item' in request.POST:
|
||||||
if form.is_valid():
|
form = StockInForm(request.POST)
|
||||||
# Create Batch
|
if form.is_valid():
|
||||||
batch = Batch.objects.create(
|
# Create Batch
|
||||||
medicine=form.cleaned_data['medicine'],
|
batch = Batch.objects.create(
|
||||||
faktur=faktur,
|
medicine=form.cleaned_data['medicine'],
|
||||||
batch_number=form.cleaned_data['batch_number'],
|
faktur=faktur,
|
||||||
expiry_date=form.cleaned_data['expiry_date'],
|
batch_number=form.cleaned_data['batch_number'],
|
||||||
quantity=form.cleaned_data['quantity'],
|
expiry_date=form.cleaned_data['expiry_date'],
|
||||||
buying_price=form.cleaned_data['buying_price'],
|
quantity=form.cleaned_data['quantity'],
|
||||||
selling_price=form.cleaned_data['selling_price']
|
buying_price=form.cleaned_data['buying_price'],
|
||||||
)
|
selling_price=form.cleaned_data['selling_price']
|
||||||
# Create Transaction
|
)
|
||||||
StockTransaction.objects.create(
|
# Create Transaction
|
||||||
medicine=batch.medicine,
|
StockTransaction.objects.create(
|
||||||
batch=batch,
|
medicine=batch.medicine,
|
||||||
faktur=faktur,
|
batch=batch,
|
||||||
transaction_type='IN',
|
faktur=faktur,
|
||||||
quantity=batch.quantity,
|
transaction_type='IN',
|
||||||
note=f"Input dari Faktur {faktur.faktur_number}"
|
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)
|
messages.success(request, f"Barang {batch.medicine.name} berhasil ditambahkan.")
|
||||||
|
return redirect('faktur_detail', pk=pk)
|
||||||
else:
|
else:
|
||||||
form = StockInForm()
|
form = StockInForm()
|
||||||
|
|
||||||
items = Batch.objects.filter(faktur=faktur)
|
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', {
|
return render(request, 'core/faktur_detail.html', {
|
||||||
'faktur': faktur,
|
'faktur': faktur,
|
||||||
'form': form,
|
'form': form,
|
||||||
'items': items,
|
'items': items,
|
||||||
'project_name': 'DN-WRS'
|
'cart': cart if show_cart else []
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -333,10 +361,12 @@ def barang_keluar(request):
|
|||||||
form = StockOutForm()
|
form = StockOutForm()
|
||||||
|
|
||||||
transactions = StockTransaction.objects.filter(transaction_type='OUT').order_by('-created_at')[:10]
|
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', {
|
return render(request, 'core/barang_keluar.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
'transactions': transactions,
|
'transactions': transactions,
|
||||||
'project_name': 'DN-WRS'
|
'cart': cart
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -349,8 +379,7 @@ def get_batches(request):
|
|||||||
def laporan_transaksi(request):
|
def laporan_transaksi(request):
|
||||||
transactions = StockTransaction.objects.all().order_by('-created_at')
|
transactions = StockTransaction.objects.all().order_by('-created_at')
|
||||||
return render(request, 'core/laporan_transaksi.html', {
|
return render(request, 'core/laporan_transaksi.html', {
|
||||||
'transactions': transactions,
|
'transactions': transactions
|
||||||
'project_name': 'DN-WRS'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -398,6 +427,195 @@ def edit_transaksi(request, pk):
|
|||||||
return redirect('laporan_transaksi')
|
return redirect('laporan_transaksi')
|
||||||
|
|
||||||
return render(request, 'core/edit_transaksi.html', {
|
return render(request, 'core/edit_transaksi.html', {
|
||||||
'transaction': transaction,
|
'transaction': transaction
|
||||||
'project_name': 'DN-WRS'
|
})
|
||||||
})
|
|
||||||
|
# --- 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
|
Django==5.2.7
|
||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
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