Autosave: 20260206-112737
This commit is contained in:
parent
7af198c681
commit
28bc475346
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -133,9 +133,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = 'id'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = 'Asia/Jakarta'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -180,3 +180,5 @@ if EMAIL_USE_SSL:
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGOUT_REDIRECT_URL = '/accounts/login/'
|
||||
|
||||
@ -21,9 +21,10 @@ from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
path("", include("core.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,39 @@
|
||||
from django.contrib import admin
|
||||
from .models import Category, Medicine, Batch, StockTransaction, Supplier, Faktur
|
||||
|
||||
# Register your models here.
|
||||
# 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(Supplier)
|
||||
class SupplierAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'contact_person', 'phone')
|
||||
search_fields = ('name',)
|
||||
|
||||
@admin.register(Faktur)
|
||||
class FakturAdmin(admin.ModelAdmin):
|
||||
list_display = ('faktur_number', 'supplier', 'date', 'faktur_type')
|
||||
list_filter = ('faktur_type', 'date')
|
||||
search_fields = ('faktur_number',)
|
||||
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
|
||||
@admin.register(Medicine)
|
||||
class MedicineAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'category', 'sku', 'unit', 'total_stock', 'status')
|
||||
search_fields = ('name', 'sku')
|
||||
list_filter = ('category', 'unit')
|
||||
|
||||
@admin.register(Batch)
|
||||
class BatchAdmin(admin.ModelAdmin):
|
||||
list_display = ('medicine', 'batch_number', 'expiry_date', 'quantity', 'is_expired')
|
||||
list_filter = ('expiry_date',)
|
||||
search_fields = ('batch_number', 'medicine__name')
|
||||
|
||||
@admin.register(StockTransaction)
|
||||
class StockTransactionAdmin(admin.ModelAdmin):
|
||||
list_display = ('medicine', 'transaction_type', 'quantity', 'created_at')
|
||||
list_filter = ('transaction_type', 'created_at')
|
||||
107
core/forms.py
Normal file
107
core/forms.py
Normal file
@ -0,0 +1,107 @@
|
||||
from django import forms
|
||||
from .models import Supplier, Faktur, Medicine, Batch, StockTransaction, Category
|
||||
|
||||
class CategoryForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ['name', 'description']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
class SupplierForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Supplier
|
||||
fields = ['name', 'contact_person', 'phone', 'address']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'contact_person': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
class MedicineForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Medicine
|
||||
fields = ['name', 'category', 'sku', 'unit', 'min_stock', 'main_supplier', 'alternative_supplier']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'category': forms.Select(attrs={'class': 'form-control'}),
|
||||
'sku': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'unit': forms.Select(attrs={'class': 'form-control'}),
|
||||
'min_stock': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'main_supplier': forms.Select(attrs={'class': 'form-control'}),
|
||||
'alternative_supplier': forms.Select(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
class FakturForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Faktur
|
||||
fields = ['faktur_number', 'supplier', 'date', 'faktur_type', 'notes']
|
||||
widgets = {
|
||||
'faktur_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'supplier': forms.Select(attrs={'class': 'form-control'}),
|
||||
'date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'faktur_type': forms.Select(attrs={'class': 'form-control'}),
|
||||
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
||||
}
|
||||
|
||||
class StockInForm(forms.Form):
|
||||
medicine = forms.ModelChoiceField(
|
||||
queryset=Medicine.objects.all(),
|
||||
label="Nama Barang",
|
||||
widget=forms.Select(attrs={'class': 'form-control select2'})
|
||||
)
|
||||
batch_number = forms.CharField(
|
||||
label="Nomor Batch/Lot",
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||
)
|
||||
expiry_date = forms.DateField(
|
||||
label="Tanggal Kadaluarsa",
|
||||
widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'})
|
||||
)
|
||||
quantity = forms.IntegerField(
|
||||
label="Jumlah",
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'})
|
||||
)
|
||||
buying_price = forms.DecimalField(
|
||||
label="Harga Beli",
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'})
|
||||
)
|
||||
selling_price = forms.DecimalField(
|
||||
label="Harga Jual",
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'})
|
||||
)
|
||||
|
||||
class StockOutForm(forms.Form):
|
||||
medicine = forms.ModelChoiceField(
|
||||
queryset=Medicine.objects.all(),
|
||||
label="Pilih Barang",
|
||||
widget=forms.Select(attrs={'class': 'form-control select2', 'id': 'id_medicine_out'})
|
||||
)
|
||||
batch = forms.ModelChoiceField(
|
||||
queryset=Batch.objects.none(),
|
||||
label="Pilih Batch",
|
||||
widget=forms.Select(attrs={'class': 'form-control', 'id': 'id_batch_out'})
|
||||
)
|
||||
quantity = forms.IntegerField(
|
||||
label="Jumlah Keluar",
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'})
|
||||
)
|
||||
note = forms.CharField(
|
||||
label="Keterangan",
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'class': 'form-control'})
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'medicine' in self.data:
|
||||
try:
|
||||
medicine_id = int(self.data.get('medicine'))
|
||||
self.fields['batch'].queryset = Batch.objects.filter(medicine_id=medicine_id, quantity__gt=0)
|
||||
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)
|
||||
64
core/migrations/0001_initial.py
Normal file
64
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-06 09:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Categories',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Medicine',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('sku', models.CharField(max_length=50, unique=True)),
|
||||
('unit', models.CharField(choices=[('Tablet', 'Tablet'), ('Kapsul', 'Kapsul'), ('Botol', 'Botol'), ('Strip', 'Strip'), ('Pcs', 'Pcs'), ('Box', 'Box')], default='Tablet', max_length=20)),
|
||||
('min_stock', models.IntegerField(default=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='medicines', to='core.category')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Batch',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('batch_number', models.CharField(max_length=100)),
|
||||
('expiry_date', models.DateField()),
|
||||
('quantity', models.IntegerField(default=0)),
|
||||
('buying_price', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('selling_price', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('medicine', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batches', to='core.medicine')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StockTransaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('transaction_type', models.CharField(choices=[('IN', 'Barang Masuk'), ('OUT', 'Barang Keluar'), ('ADJ', 'Penyesuaian')], max_length=3)),
|
||||
('quantity', models.IntegerField()),
|
||||
('note', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.batch')),
|
||||
('medicine', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.medicine')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,170 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-06 10:18
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Faktur',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('faktur_number', models.CharField(max_length=100, unique=True, verbose_name='Nomor Faktur')),
|
||||
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Tanggal')),
|
||||
('faktur_type', models.CharField(choices=[('MASUK', 'Faktur Masuk (Supplier)'), ('KELUAR', 'Faktur Keluar')], default='MASUK', max_length=10, verbose_name='Tipe Faktur')),
|
||||
('notes', models.TextField(blank=True, verbose_name='Catatan')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Faktur',
|
||||
'verbose_name_plural': 'Faktur',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Supplier',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Nama Supplier')),
|
||||
('contact_person', models.CharField(blank=True, max_length=100, verbose_name='Kontak Person')),
|
||||
('phone', models.CharField(blank=True, max_length=20, verbose_name='Telepon')),
|
||||
('address', models.TextField(blank=True, verbose_name='Alamat')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Supplier',
|
||||
'verbose_name_plural': 'Supplier',
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='batch',
|
||||
options={'verbose_name': 'Batch Barang', 'verbose_name_plural': 'Batch Barang'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='category',
|
||||
options={'verbose_name': 'Kategori', 'verbose_name_plural': 'Kategori'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='medicine',
|
||||
options={'verbose_name': 'Barang/Obat', 'verbose_name_plural': 'Barang/Obat'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='stocktransaction',
|
||||
options={'verbose_name': 'Transaksi Stok', 'verbose_name_plural': 'Transaksi Stok'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='batch',
|
||||
name='batch_number',
|
||||
field=models.CharField(max_length=100, verbose_name='Nomor Batch/Lot'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='batch',
|
||||
name='buying_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Harga Beli'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='batch',
|
||||
name='expiry_date',
|
||||
field=models.DateField(verbose_name='Tanggal Kadaluarsa'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='batch',
|
||||
name='medicine',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batches', to='core.medicine', verbose_name='Barang'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='batch',
|
||||
name='quantity',
|
||||
field=models.IntegerField(default=0, verbose_name='Jumlah Stok'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='batch',
|
||||
name='selling_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Harga Jual'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, verbose_name='Deskripsi'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, verbose_name='Nama Kategori'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='medicine',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='medicines', to='core.category', verbose_name='Kategori'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='medicine',
|
||||
name='min_stock',
|
||||
field=models.IntegerField(default=10, verbose_name='Stok Minimal'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='medicine',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255, verbose_name='Nama Barang'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='medicine',
|
||||
name='sku',
|
||||
field=models.CharField(max_length=50, unique=True, verbose_name='SKU/Kode'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='medicine',
|
||||
name='unit',
|
||||
field=models.CharField(choices=[('Tablet', 'Tablet'), ('Kapsul', 'Kapsul'), ('Botol', 'Botol'), ('Strip', 'Strip'), ('Pcs', 'Pcs'), ('Box', 'Box')], default='Tablet', max_length=20, verbose_name='Satuan'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stocktransaction',
|
||||
name='batch',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='core.batch', verbose_name='Batch'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stocktransaction',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Waktu'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stocktransaction',
|
||||
name='medicine',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.medicine', verbose_name='Barang'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stocktransaction',
|
||||
name='note',
|
||||
field=models.TextField(blank=True, verbose_name='Keterangan'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stocktransaction',
|
||||
name='quantity',
|
||||
field=models.IntegerField(verbose_name='Jumlah'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stocktransaction',
|
||||
name='transaction_type',
|
||||
field=models.CharField(choices=[('IN', 'Barang Masuk'), ('OUT', 'Barang Keluar'), ('ADJ', 'Penyesuaian')], max_length=3, verbose_name='Tipe Transaksi'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='faktur',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='batches', to='core.faktur', verbose_name='Faktur'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='stocktransaction',
|
||||
name='faktur',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='core.faktur', verbose_name='Faktur'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='faktur',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.supplier', verbose_name='Supplier'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-06 10:56
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_faktur_supplier_alter_batch_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='medicine',
|
||||
name='alternative_supplier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alternative_medicines', to='core.supplier', verbose_name='Supplier Alternatif'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='medicine',
|
||||
name='main_supplier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_medicines', to='core.supplier', verbose_name='Supplier Utama'),
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
133
core/models.py
133
core/models.py
@ -1,3 +1,134 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
# Create your models here.
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name="Nama Kategori")
|
||||
description = models.TextField(blank=True, verbose_name="Deskripsi")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Kategori"
|
||||
verbose_name = "Kategori"
|
||||
|
||||
class Supplier(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name="Nama Supplier")
|
||||
contact_person = models.CharField(max_length=100, blank=True, verbose_name="Kontak Person")
|
||||
phone = models.CharField(max_length=20, blank=True, verbose_name="Telepon")
|
||||
address = models.TextField(blank=True, verbose_name="Alamat")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Supplier"
|
||||
verbose_name = "Supplier"
|
||||
|
||||
class Medicine(models.Model):
|
||||
UNIT_CHOICES = [
|
||||
('Tablet', 'Tablet'),
|
||||
('Kapsul', 'Kapsul'),
|
||||
('Botol', 'Botol'),
|
||||
('Strip', 'Strip'),
|
||||
('Pcs', 'Pcs'),
|
||||
('Box', 'Box'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=255, verbose_name="Nama Barang")
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='medicines', verbose_name="Kategori")
|
||||
sku = models.CharField(max_length=50, unique=True, verbose_name="SKU/Kode")
|
||||
unit = models.CharField(max_length=20, choices=UNIT_CHOICES, default='Tablet', verbose_name="Satuan")
|
||||
min_stock = models.IntegerField(default=10, verbose_name="Stok Minimal")
|
||||
main_supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, related_name='primary_medicines', verbose_name="Supplier Utama")
|
||||
alternative_supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, related_name='alternative_medicines', verbose_name="Supplier Alternatif")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def total_stock(self):
|
||||
return sum(batch.quantity for batch in self.batches.all())
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
total = self.total_stock
|
||||
if total <= 0:
|
||||
return "Habis"
|
||||
if total <= self.min_stock:
|
||||
return "Stok Menipis"
|
||||
return "Tersedia"
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Barang/Obat"
|
||||
verbose_name = "Barang/Obat"
|
||||
|
||||
class Faktur(models.Model):
|
||||
FAKTUR_TYPE_CHOICES = [
|
||||
('MASUK', 'Faktur Masuk (Supplier)'),
|
||||
('KELUAR', 'Faktur Keluar'),
|
||||
]
|
||||
faktur_number = models.CharField(max_length=100, unique=True, verbose_name="Nomor Faktur")
|
||||
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Supplier")
|
||||
date = models.DateField(default=timezone.now, verbose_name="Tanggal")
|
||||
faktur_type = models.CharField(max_length=10, choices=FAKTUR_TYPE_CHOICES, default='MASUK', verbose_name="Tipe Faktur")
|
||||
notes = models.TextField(blank=True, verbose_name="Catatan")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.faktur_number
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Faktur"
|
||||
verbose_name = "Faktur"
|
||||
|
||||
class Batch(models.Model):
|
||||
medicine = models.ForeignKey(Medicine, on_delete=models.CASCADE, related_name='batches', verbose_name="Barang")
|
||||
faktur = models.ForeignKey(Faktur, on_delete=models.SET_NULL, null=True, blank=True, related_name='batches', verbose_name="Faktur")
|
||||
batch_number = models.CharField(max_length=100, verbose_name="Nomor Batch/Lot")
|
||||
expiry_date = models.DateField(verbose_name="Tanggal Kadaluarsa")
|
||||
quantity = models.IntegerField(default=0, verbose_name="Jumlah Stok")
|
||||
buying_price = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Harga Beli")
|
||||
selling_price = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Harga Jual")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.medicine.name} - {self.batch_number}"
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
return self.expiry_date <= timezone.now().date()
|
||||
|
||||
@property
|
||||
def is_near_expiry(self):
|
||||
today = timezone.now().date()
|
||||
diff = self.expiry_date - today
|
||||
return 0 < diff.days <= 90 # 3 months
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Batch Barang"
|
||||
verbose_name = "Batch Barang"
|
||||
|
||||
class StockTransaction(models.Model):
|
||||
TRANSACTION_TYPES = [
|
||||
('IN', 'Barang Masuk'),
|
||||
('OUT', 'Barang Keluar'),
|
||||
('ADJ', 'Penyesuaian'),
|
||||
]
|
||||
|
||||
medicine = models.ForeignKey(Medicine, on_delete=models.CASCADE, verbose_name="Barang")
|
||||
batch = models.ForeignKey(Batch, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions', verbose_name="Batch")
|
||||
faktur = models.ForeignKey(Faktur, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions', verbose_name="Faktur")
|
||||
transaction_type = models.CharField(max_length=3, choices=TRANSACTION_TYPES, verbose_name="Tipe Transaksi")
|
||||
quantity = models.IntegerField(verbose_name="Jumlah")
|
||||
note = models.TextField(blank=True, verbose_name="Keterangan")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Waktu")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_transaction_type_display()} - {self.medicine.name} ({self.quantity})"
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Transaksi Stok"
|
||||
verbose_name = "Transaksi Stok"
|
||||
@ -1,25 +1,128 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ project_name|default:"DN-WRS" }}</title>
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Google Fonts: Plus Jakarta Sans -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<!-- Lucide Icons -->
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={% now 'U' %}">
|
||||
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</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>
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
</html>
|
||||
<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>
|
||||
|
||||
<main class="container py-2 fade-in">
|
||||
{% block content %}{% 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>
|
||||
|
||||
<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');
|
||||
});
|
||||
</script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
152
core/templates/core/barang_keluar.html
Normal file
152
core/templates/core/barang_keluar.html
Normal file
@ -0,0 +1,152 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<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">
|
||||
<i data-lucide="file-minus"></i>
|
||||
</div>
|
||||
Barang Keluar
|
||||
</h4>
|
||||
<form method="post" id="outForm">
|
||||
{% csrf_token %}
|
||||
<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;">PILIH BARANG</label>
|
||||
{{ form.medicine }}
|
||||
</div>
|
||||
<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;">PILIH BATCH (STOK TERSEDIA)</label>
|
||||
{{ form.batch }}
|
||||
<div id="stock-hint" class="small text-primary mt-2 fw-bold"></div>
|
||||
</div>
|
||||
<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;">JUMLAH KELUAR</label>
|
||||
{{ form.quantity }}
|
||||
</div>
|
||||
<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;">KETERANGAN</label>
|
||||
{{ form.note }}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger w-100 py-3 mt-3 shadow-sm d-flex align-items-center justify-content-center">
|
||||
<i data-lucide="minus-circle" class="me-2 icon-sm"></i> Catat Barang Keluar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="history"></i>
|
||||
</div>
|
||||
Transaksi Terakhir
|
||||
</h4>
|
||||
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">10 Transaksi</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Barang</th>
|
||||
<th>Batch</th>
|
||||
<th class="text-end">Jumlah</th>
|
||||
<th>Waktu</th>
|
||||
<th class="text-end">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in transactions %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold text-dark">{{ t.medicine.name }}</div>
|
||||
<div class="small text-muted">{{ t.note|default:"-" }}</div>
|
||||
</td>
|
||||
<td><code class="text-primary">{{ t.batch.batch_number }}</code></td>
|
||||
<td class="text-end fw-bold text-danger">-{{ t.quantity }}</td>
|
||||
<td><span class="small text-muted">{{ t.created_at|date:"d/m/y H:i" }}</span></td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'edit_transaksi' t.pk %}" class="btn btn-light btn-sm rounded-circle p-2">
|
||||
<i data-lucide="edit-3" class="icon-sm"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">
|
||||
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
|
||||
Belum ada transaksi keluar.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
.select2-container--default .select2-selection--single {
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 48px;
|
||||
border-radius: 0.75rem;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 46px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#id_medicine_out').select2({
|
||||
placeholder: "Pilih Barang",
|
||||
allowClear: true
|
||||
});
|
||||
|
||||
$('#id_medicine_out').on('change', function() {
|
||||
var medicineId = $(this).val();
|
||||
var batchSelect = $('#id_batch_out');
|
||||
batchSelect.empty().append('<option value="">Memuat...</option>');
|
||||
|
||||
if (medicineId) {
|
||||
$.ajax({
|
||||
url: "{% url 'get_batches' %}",
|
||||
data: { 'medicine_id': medicineId },
|
||||
success: function(data) {
|
||||
batchSelect.empty().append('<option value="">Pilih Batch</option>');
|
||||
data.forEach(function(batch) {
|
||||
batchSelect.append('<option value="' + batch.id + '" data-qty="' + batch.quantity + '">' + batch.batch_number + ' (Stok: ' + batch.quantity + ')</option>');
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
batchSelect.empty().append('<option value="">Pilih Barang Terlebih Dahulu</option>');
|
||||
}
|
||||
});
|
||||
|
||||
$('#id_batch_out').on('change', function() {
|
||||
var selected = $(this).find('option:selected');
|
||||
var qty = selected.data('qty');
|
||||
if (qty !== undefined) {
|
||||
$('#stock-hint').text('Stok tersedia: ' + qty);
|
||||
} else {
|
||||
$('#stock-hint').text('');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
83
core/templates/core/categories.html
Normal file
83
core/templates/core/categories.html
Normal file
@ -0,0 +1,83 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="fw-bold mb-4 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="tag"></i>
|
||||
</div>
|
||||
Tambah Kategori
|
||||
</h4>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% 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.help_text %}
|
||||
<div class="form-text small">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</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">
|
||||
<i data-lucide="save" class="me-2 icon-sm"></i> Simpan Kategori
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="layers"></i>
|
||||
</div>
|
||||
Daftar Kategori
|
||||
</h4>
|
||||
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ categories|length }} Total</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nama Kategori</th>
|
||||
<th>Deskripsi</th>
|
||||
<th class="text-end">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in categories %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold text-dark">{{ c.name }}</div>
|
||||
</td>
|
||||
<td><span class="text-muted small">{{ c.description|default:"-"|truncatechars:100 }}</span></td>
|
||||
<td class="text-end">
|
||||
<a href="/admin/core/category/{{ c.id }}/change/" class="btn btn-light btn-sm rounded-circle p-2">
|
||||
<i data-lucide="edit-3" class="icon-sm"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-5 text-muted">
|
||||
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
|
||||
Belum ada data kategori.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
61
core/templates/core/edit_transaksi.html
Normal file
61
core/templates/core/edit_transaksi.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<div class="card-body p-4 p-lg-5">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'laporan_transaksi' %}" class="text-decoration-none">Laporan</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Edit Transaksi</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h3 class="fw-bold mb-4 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="edit-3"></i>
|
||||
</div>
|
||||
Edit Transaksi
|
||||
</h3>
|
||||
|
||||
<div class="p-4 bg-light rounded-4 mb-4 border border-white">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">BARANG</small>
|
||||
<span class="fw-bold text-dark">{{ transaction.medicine.name }}</span>
|
||||
</div>
|
||||
<div class="col-sm-6 text-sm-end">
|
||||
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">TIPE</small>
|
||||
<span class="badge {% if transaction.transaction_type == 'IN' %}bg-success-subtle text-success{% else %}bg-danger-subtle text-danger{% endif %} rounded-pill px-3">
|
||||
{{ transaction.get_transaction_type_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 mt-3">
|
||||
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">BATCH NUMBER</small>
|
||||
<code class="text-primary fs-6">{{ transaction.batch.batch_number }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-4">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">JUMLAH</label>
|
||||
<input type="number" name="quantity" class="form-control form-control-lg rounded-3" value="{{ transaction.quantity }}" required>
|
||||
<div class="form-text small mt-2">Perubahan jumlah akan otomatis memperbarui stok pada batch.</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label small fw-bold text-muted text-uppercase mb-1" style="font-size: 0.65rem; letter-spacing: 0.5px;">KETERANGAN</label>
|
||||
<textarea name="note" class="form-control rounded-3" rows="3">{{ transaction.note }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 mt-5">
|
||||
<a href="{% url 'laporan_transaksi' %}" class="btn btn-light flex-grow-1 py-3 fw-bold">Batal</a>
|
||||
<button type="submit" class="btn btn-primary flex-grow-1 py-3 fw-bold shadow-sm">Simpan Perubahan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
131
core/templates/core/faktur_detail.html
Normal file
131
core/templates/core/faktur_detail.html
Normal file
@ -0,0 +1,131 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="mb-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'input_faktur' %}" class="text-decoration-none">Riwayat Faktur</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Detail Faktur {{ faktur.faktur_number }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="card glass-card border-0 shadow-sm p-4 mb-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<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
|
||||
</h5>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% 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">
|
||||
<i data-lucide="plus" class="me-2 icon-sm"></i> Tambah Barang
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="fw-bold mb-0 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="list"></i>
|
||||
</div>
|
||||
Rincian Barang
|
||||
</h5>
|
||||
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ items|length }} Item</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Barang</th>
|
||||
<th>No. Batch</th>
|
||||
<th>Kadaluarsa</th>
|
||||
<th class="text-end">Jumlah</th>
|
||||
<th class="text-end">Harga Beli</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold text-dark">{{ item.medicine.name }}</div>
|
||||
<div class="small text-muted">{{ item.medicine.sku }}</div>
|
||||
</td>
|
||||
<td><code class="text-primary">{{ item.batch_number }}</code></td>
|
||||
<td><span class="small text-muted">{{ item.expiry_date|date:"d M Y" }}</span></td>
|
||||
<td class="text-end fw-bold text-dark">{{ item.quantity }} <span class="text-muted small">{{ item.medicine.unit }}</span></td>
|
||||
<td class="text-end text-success fw-semibold">Rp {{ item.buying_price|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">
|
||||
<i data-lucide="package-search" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
|
||||
<p class="mb-0">Belum ada barang diinput untuk faktur ini.</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_head %}
|
||||
<!-- Select2 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
.select2-container--default .select2-selection--single {
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 48px;
|
||||
border-radius: 0.75rem;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 46px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.select2').select2({
|
||||
placeholder: "Pilih Barang",
|
||||
allowClear: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,145 +1,260 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- Stats Cards -->
|
||||
<div class="col-md-3">
|
||||
<div class="card glass-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stats-icon bg-primary-subtle text-primary">
|
||||
<i data-lucide="package"></i>
|
||||
</div>
|
||||
<h6 class="text-muted mb-1 small fw-bold text-uppercase">TOTAL BARANG</h6>
|
||||
<h2 class="fw-bold mb-0 text-dark">{{ total_medicines }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
<div class="col-md-3">
|
||||
<div class="card glass-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stats-icon bg-success-subtle text-success">
|
||||
<i data-lucide="boxes"></i>
|
||||
</div>
|
||||
<h6 class="text-muted mb-1 small fw-bold text-uppercase">TOTAL STOK</h6>
|
||||
<h2 class="fw-bold mb-0 text-dark">{{ total_stock }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card glass-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stats-icon bg-warning-subtle text-warning">
|
||||
<i data-lucide="alert-triangle"></i>
|
||||
</div>
|
||||
<h6 class="text-muted mb-1 small fw-bold text-uppercase">STOK MENIPIS</h6>
|
||||
<h2 class="fw-bold mb-0 text-dark">{{ low_stock_count }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card glass-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="stats-icon bg-danger-subtle text-danger">
|
||||
<i data-lucide="calendar-x"></i>
|
||||
</div>
|
||||
<h6 class="text-muted mb-1 small fw-bold text-uppercase">KADALUARSA</h6>
|
||||
<h2 class="fw-bold mb-0 text-dark">{{ expired_count }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- Chart Area -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card glass-card h-100">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="fw-bold mb-0">Pergerakan Stok</h5>
|
||||
<span class="badge bg-light text-dark border py-2 px-3 rounded-pill small">7 Hari Terakhir</span>
|
||||
</div>
|
||||
<div style="height: 300px;">
|
||||
<canvas id="movementChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expiry Overview -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 rounded-4 h-100 bg-white shadow-sm overflow-hidden">
|
||||
<div class="card-body p-0">
|
||||
<div class="p-4 bg-danger text-white">
|
||||
<h5 class="fw-bold mb-0">Pantauan Kadaluarsa</h5>
|
||||
<p class="small opacity-75 mb-0">Segera kadaluarsa (90 hari ke depan)</p>
|
||||
</div>
|
||||
<div class="p-4 text-center">
|
||||
<div class="display-1 fw-bold text-danger mb-2">{{ near_expiry_count }}</div>
|
||||
<p class="text-muted mb-4">Item memerlukan perhatian segera</p>
|
||||
<div class="d-grid">
|
||||
<a href="/admin/core/batch/" class="btn btn-outline-danger d-flex align-items-center justify-content-center">
|
||||
<i data-lucide="external-link" class="me-2 icon-sm"></i> Tinjau Batch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card glass-card">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="fw-bold mb-0 d-flex align-items-center">
|
||||
<i data-lucide="history" class="me-2 text-primary"></i> Data Barang Terbaru
|
||||
</h5>
|
||||
<a href="/admin/core/medicine/" class="btn btn-primary btn-sm d-flex align-items-center px-3 rounded-pill">
|
||||
<i data-lucide="settings" class="me-2 icon-sm"></i> Kelola Semua
|
||||
</a>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Nama Barang</th>
|
||||
<th>Kategori</th>
|
||||
<th>Stok</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for medicine in recent_medicines %}
|
||||
<tr>
|
||||
<td><span class="badge bg-light text-muted">{{ medicine.sku }}</span></td>
|
||||
<td><span class="fw-bold">{{ medicine.name }}</span></td>
|
||||
<td><span class="text-muted small">{{ medicine.category.name }}</span></td>
|
||||
<td>
|
||||
<span class="fw-bold">{{ medicine.total_stock }}</span>
|
||||
<span class="text-muted small ms-1">{{ medicine.unit }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if medicine.status == 'Tersedia' %}
|
||||
<span class="badge bg-success-subtle text-success rounded-pill px-3">Tersedia</span>
|
||||
{% elif medicine.status == 'Stok Menipis' %}
|
||||
<span class="badge bg-warning-subtle text-warning rounded-pill px-3">Stok Menipis</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">Habis</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="/admin/core/medicine/{{ medicine.id }}/change/" class="btn btn-light btn-sm rounded-circle p-2">
|
||||
<i data-lucide="edit-3" class="icon-sm"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
|
||||
Belum ada data tersedia.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
const ctx = document.getElementById('movementChart').getContext('2d');
|
||||
|
||||
// Create gradients
|
||||
const gradientIn = ctx.createLinearGradient(0, 0, 0, 300);
|
||||
gradientIn.addColorStop(0, 'rgba(67, 97, 238, 0.2)');
|
||||
gradientIn.addColorStop(1, 'rgba(67, 97, 238, 0)');
|
||||
|
||||
const gradientOut = ctx.createLinearGradient(0, 0, 0, 300);
|
||||
gradientOut.addColorStop(0, 'rgba(239, 35, 60, 0.2)');
|
||||
gradientOut.addColorStop(1, 'rgba(239, 35, 60, 0)');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {{ chart_labels|safe }},
|
||||
datasets: [
|
||||
{
|
||||
label: 'Barang Masuk',
|
||||
data: {{ chart_data_in|safe }},
|
||||
borderColor: '#4361ee',
|
||||
backgroundColor: gradientIn,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
borderWidth: 3,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 2
|
||||
},
|
||||
{
|
||||
label: 'Barang Keluar',
|
||||
data: {{ chart_data_out|safe }},
|
||||
borderColor: '#ef233c',
|
||||
backgroundColor: gradientOut,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
borderWidth: 3,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
align: 'end',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
boxWidth: 8,
|
||||
font: {
|
||||
family: 'Plus Jakarta Sans',
|
||||
size: 12,
|
||||
weight: '600'
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
padding: 12,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
titleColor: '#1e293b',
|
||||
titleFont: { size: 14, weight: 'bold' },
|
||||
bodyColor: '#64748b',
|
||||
borderColor: '#e2e8f0',
|
||||
borderWidth: 1,
|
||||
displayColors: true,
|
||||
boxPadding: 6,
|
||||
usePointStyle: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.03)',
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
font: { family: 'Plus Jakarta Sans' }
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
font: { family: 'Plus Jakarta Sans' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
86
core/templates/core/input_faktur.html
Normal file
86
core/templates/core/input_faktur.html
Normal file
@ -0,0 +1,86 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="fw-bold mb-4 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="file-plus"></i>
|
||||
</div>
|
||||
Input Faktur Baru
|
||||
</h4>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in faktur_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">
|
||||
<i data-lucide="arrow-right" class="me-2 icon-sm"></i> Simpan & Lanjutkan
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="history"></i>
|
||||
</div>
|
||||
Riwayat Faktur
|
||||
</h4>
|
||||
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ fakturs|length }} Total</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No. Faktur</th>
|
||||
<th>Supplier</th>
|
||||
<th>Tanggal</th>
|
||||
<th>Tipe</th>
|
||||
<th class="text-end">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in fakturs %}
|
||||
<tr>
|
||||
<td class="fw-bold text-dark">{{ f.faktur_number }}</td>
|
||||
<td>{{ f.supplier.name|default:"-" }}</td>
|
||||
<td><span class="text-muted small">{{ f.date }}</span></td>
|
||||
<td>
|
||||
<span class="badge {% if f.faktur_type == 'MASUK' %}bg-info-subtle text-info{% else %}bg-warning-subtle text-warning{% endif %} rounded-pill px-3">
|
||||
{{ f.get_faktur_type_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'faktur_detail' f.pk %}" class="btn btn-light btn-sm rounded-pill px-3 fw-bold">
|
||||
<i data-lucide="plus-circle" class="me-1 icon-sm"></i> Input Barang
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">
|
||||
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
|
||||
Belum ada data faktur.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
122
core/templates/core/laporan_transaksi.html
Normal file
122
core/templates/core/laporan_transaksi.html
Normal file
@ -0,0 +1,122 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<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-4">
|
||||
<h3 class="fw-bold mb-0 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="bar-chart-2"></i>
|
||||
</div>
|
||||
Laporan Keluar Masuk Barang
|
||||
</h3>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-primary rounded-pill px-4 fw-bold" onclick="window.print()">
|
||||
<i data-lucide="printer" class="icon-sm me-2"></i> Cetak Laporan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle" id="reportTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Waktu</th>
|
||||
<th>Barang</th>
|
||||
<th>Tipe</th>
|
||||
<th class="text-end">Jumlah</th>
|
||||
<th>Keterangan</th>
|
||||
<th class="text-end">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in transactions %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="small fw-bold text-dark">{{ t.created_at|date:"d M Y" }}</div>
|
||||
<div class="small text-muted">{{ t.created_at|date:"H:i" }} WIB</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold text-dark">{{ t.medicine.name }}</div>
|
||||
<div class="small text-primary fw-medium">Batch: {{ t.batch.batch_number|default:"-" }}</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if t.transaction_type == 'IN' %}
|
||||
<span class="badge bg-success-subtle text-success rounded-pill px-3">Masuk</span>
|
||||
{% elif t.transaction_type == 'OUT' %}
|
||||
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">Keluar</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary-subtle text-secondary rounded-pill px-3">Penyesuaian</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end fw-bold fs-5">
|
||||
{% if t.transaction_type == 'OUT' %}
|
||||
<span class="text-danger">-{{ t.quantity }}</span>
|
||||
{% else %}
|
||||
<span class="text-success">+{{ t.quantity }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="small text-muted">{{ t.note|default:"-" }}</span></td>
|
||||
<td class="text-end">
|
||||
<div class="d-flex justify-content-end gap-1">
|
||||
<a href="{% url 'edit_transaksi' t.pk %}" class="btn btn-light btn-sm rounded-circle p-2" title="Edit">
|
||||
<i data-lucide="edit-3" class="icon-sm"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-light btn-sm rounded-circle p-2" title="Hapus"
|
||||
onclick="confirmDelete('{{ t.pk }}', '{{ t.medicine.name }}')">
|
||||
<i data-lucide="trash-2" class="icon-sm text-danger"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
|
||||
Belum ada transaksi stok.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Konfirmasi Hapus -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg rounded-4">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title fw-bold">Konfirmasi Hapus</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body py-4 text-center">
|
||||
<div class="bg-danger-subtle text-danger p-3 rounded-circle d-inline-flex mb-3">
|
||||
<i data-lucide="alert-triangle" style="width: 32px; height: 32px;"></i>
|
||||
</div>
|
||||
<h5>Hapus Transaksi?</h5>
|
||||
<p class="text-muted">Apakah Anda yakin ingin menghapus transaksi <span id="deleteItemName" class="fw-bold text-dark"></span>? Tindakan ini akan mengembalikan stok pada batch terkait.</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0 justify-content-center pb-4">
|
||||
<button type="button" class="btn btn-light rounded-pill px-4 fw-bold" data-bs-dismiss="modal">Batal</button>
|
||||
<form id="deleteForm" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger rounded-pill px-4 fw-bold shadow-sm">Ya, Hapus Transaksi</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function confirmDelete(pk, name) {
|
||||
document.getElementById('deleteItemName').innerText = name;
|
||||
document.getElementById('deleteForm').action = "/transaksi/" + pk + "/delete/";
|
||||
var myModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
myModal.show();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
238
core/templates/core/medicines.html
Normal file
238
core/templates/core/medicines.html
Normal file
@ -0,0 +1,238 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="bg-primary-subtle text-primary p-3 rounded-circle d-inline-flex mb-3">
|
||||
<i data-lucide="package" style="width: 24px; height: 24px;"></i>
|
||||
</div>
|
||||
<h6 class="text-muted fw-bold mb-1">Total Barang</h6>
|
||||
<h2 class="fw-bold mb-0 text-dark">{{ total_count }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="bg-warning-subtle text-warning p-3 rounded-circle d-inline-flex mb-3">
|
||||
<i data-lucide="alert-triangle" style="width: 24px; height: 24px;"></i>
|
||||
</div>
|
||||
<h6 class="text-muted fw-bold mb-1">Stok Menipis</h6>
|
||||
<h2 class="fw-bold mb-0 text-warning">{{ low_stock_count }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4 text-center">
|
||||
<div class="bg-success-subtle text-success p-3 rounded-circle d-inline-flex mb-3">
|
||||
<i data-lucide="check-circle" style="width: 24px; height: 24px;"></i>
|
||||
</div>
|
||||
<h6 class="text-muted fw-bold mb-1">Status Aman</h6>
|
||||
<h2 class="fw-bold mb-0 text-success">{{ total_count|add:"-"|add:low_stock_count|default:0|cut:"-" }}</h2>
|
||||
</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 flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3">
|
||||
<h3 class="fw-bold mb-0 d-flex align-items-center text-dark">
|
||||
<div class="bg-primary text-white p-2 rounded-3 me-3">
|
||||
<i data-lucide="pill"></i>
|
||||
</div>
|
||||
Manajemen Data Barang
|
||||
</h3>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm d-flex align-items-center" data-bs-toggle="modal" data-bs-target="#addMedicineModal">
|
||||
<i data-lucide="plus-circle" class="me-2 icon-sm"></i> Tambah Barang Baru
|
||||
</button>
|
||||
<a href="{% url 'export_low_stock_pdf' %}" class="btn btn-outline-danger rounded-pill px-4 fw-bold d-flex align-items-center">
|
||||
<i data-lucide="file-down" class="me-2 icon-sm"></i> Export Stok Menipis (PDF)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="opacity-10 mb-4">
|
||||
|
||||
<!-- Search & Filters -->
|
||||
<div class="row g-3 mb-4 align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<div class="input-group flex-grow-1">
|
||||
<span class="input-group-text bg-white border-end-0 border-opacity-25 rounded-start-pill ps-3"><i data-lucide="search" class="icon-sm text-muted"></i></span>
|
||||
<input type="text" name="q" class="form-control border-start-0 border-opacity-25 rounded-end-pill py-2" placeholder="Cari nama, SKU, atau kategori..." value="{{ query|default:'' }}">
|
||||
</div>
|
||||
{% if query %}
|
||||
<a href="{% url 'medicine_list' %}" class="btn btn-light rounded-pill px-3 d-flex align-items-center">
|
||||
<i data-lucide="x" class="icon-sm"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-secondary rounded-pill px-4 fw-bold">Cari</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-6 d-flex justify-content-lg-end gap-2">
|
||||
<div class="btn-group p-1 bg-light rounded-pill" role="group">
|
||||
<a href="{% url 'medicine_list' %}{% if query %}?q={{ query }}{% endif %}" class="btn rounded-pill px-4 fw-bold {% if not show_low_stock %}btn-white shadow-sm{% else %}btn-transparent text-muted{% endif %}">
|
||||
Semua
|
||||
</a>
|
||||
<a href="{% url 'medicine_list' %}?low_stock=1{% if query %}&q={{ query }}{% endif %}" class="btn rounded-pill px-4 fw-bold {% if show_low_stock %}btn-warning shadow-sm{% else %}btn-transparent text-muted{% endif %}">
|
||||
Stok Menipis
|
||||
{% if low_stock_count > 0 %}
|
||||
<span class="badge rounded-pill bg-danger ms-1">{{ low_stock_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light opacity-75">
|
||||
<tr>
|
||||
<th class="ps-3">SKU</th>
|
||||
<th>Nama Barang</th>
|
||||
<th>Kategori</th>
|
||||
<th>Supplier Utama</th>
|
||||
<th>Supplier Alternatif</th>
|
||||
<th class="text-center">Stok</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end pe-3">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="border-top-0">
|
||||
{% for m in medicines %}
|
||||
<tr>
|
||||
<td class="ps-3"><span class="badge bg-light text-muted border fw-normal">{{ m.sku }}</span></td>
|
||||
<td>
|
||||
<div class="fw-bold text-dark">{{ m.name }}</div>
|
||||
<div class="text-muted small d-flex align-items-center">
|
||||
<i data-lucide="box" class="icon-xs me-1"></i> {{ m.unit }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info-subtle text-info rounded-pill px-2 small">{{ m.category.name }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small fw-semibold text-dark">{{ m.main_supplier.name|default:"-" }}</div>
|
||||
{% if m.main_supplier %}
|
||||
<div class="text-muted smaller"><i data-lucide="phone" class="icon-xs me-1"></i> {{ m.main_supplier.phone|default:"N/A" }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="small text-muted">{{ m.alternative_supplier.name|default:"-" }}</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="fs-5 fw-bold {% if m.total_stock <= m.min_stock %}text-danger{% else %}text-primary{% endif %}">
|
||||
{{ m.total_stock }}
|
||||
</div>
|
||||
<div class="text-muted small">Min: {{ m.min_stock }}</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if m.status == 'Tersedia' %}
|
||||
<span class="badge bg-success-subtle text-success rounded-pill px-3 py-2 d-inline-flex align-items-center">
|
||||
<span class="p-1 bg-success rounded-circle me-2"></span> Tersedia
|
||||
</span>
|
||||
{% elif m.status == 'Stok Menipis' %}
|
||||
<span class="badge bg-warning-subtle text-warning rounded-pill px-3 py-2 d-inline-flex align-items-center">
|
||||
<span class="p-1 bg-warning rounded-circle me-2"></span> Menipis
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger-subtle text-danger rounded-pill px-3 py-2 d-inline-flex align-items-center">
|
||||
<span class="p-1 bg-danger rounded-circle me-2"></span> Habis
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<div class="btn-group">
|
||||
<a href="/admin/core/medicine/{{ m.pk }}/change/" class="btn btn-outline-primary btn-sm rounded-pill px-3 d-flex align-items-center" title="Edit Data">
|
||||
<i data-lucide="edit-2" class="icon-sm me-1"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-5">
|
||||
<div class="text-muted">
|
||||
<i data-lucide="search-x" class="mb-3 d-block mx-auto opacity-20" style="width: 64px; height: 64px;"></i>
|
||||
<h5 class="fw-bold">Tidak ada data ditemukan</h5>
|
||||
<p class="small mb-0">Coba ubah kata kunci pencarian atau filter Anda.</p>
|
||||
<a href="{% url 'medicine_list' %}" class="btn btn-link btn-sm mt-2 text-decoration-none">Reset Semua Filter</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Medicine Modal -->
|
||||
<div class="modal fade" id="addMedicineModal" tabindex="-1" aria-labelledby="addMedicineModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content glass-card shadow-lg border-0">
|
||||
<div class="modal-header border-0 pb-0 ps-4 pt-4">
|
||||
<h5 class="modal-title fw-bold d-flex align-items-center" id="addMedicineModalLabel">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-2">
|
||||
<i data-lucide="plus" class="icon-sm"></i>
|
||||
</div>
|
||||
Tambah Data Barang Baru
|
||||
</h5>
|
||||
<button type="button" class="btn-close me-2" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<p class="text-muted small mb-4">Lengkapi informasi berikut untuk menambahkan barang/obat ke dalam sistem.</p>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label small fw-bold text-dark">Nama Barang/Obat <span class="text-danger">*</span></label>
|
||||
{{ form.name }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-bold text-dark">SKU / Kode Unik <span class="text-danger">*</span></label>
|
||||
{{ form.sku }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-dark">Kategori Barang <span class="text-danger">*</span></label>
|
||||
{{ form.category }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold text-dark">Satuan <span class="text-danger">*</span></label>
|
||||
{{ form.unit }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-bold text-dark">Stok Minimal (Alert) <span class="text-danger">*</span></label>
|
||||
{{ form.min_stock }}
|
||||
<div class="form-text smaller">Sistem akan memberi peringatan jika stok di bawah angka ini.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-bold text-dark">Supplier Utama</label>
|
||||
{{ form.main_supplier }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-bold text-dark">Supplier Alternatif</label>
|
||||
{{ form.alternative_supplier }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0">
|
||||
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-primary rounded-pill px-5 fw-bold shadow-sm">Simpan Data Barang</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.smaller { font-size: 0.75rem; }
|
||||
.btn-white { background-color: white; border: 1px solid #dee2e6; }
|
||||
.btn-transparent { background-color: transparent; border: 1px solid transparent; }
|
||||
.table-hover tbody tr:hover { background-color: rgba(13, 110, 253, 0.02); }
|
||||
</style>
|
||||
{% endblock %}
|
||||
86
core/templates/core/suppliers.html
Normal file
86
core/templates/core/suppliers.html
Normal file
@ -0,0 +1,86 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="fw-bold mb-4 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="user-plus"></i>
|
||||
</div>
|
||||
Tambah Supplier
|
||||
</h4>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% 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.help_text %}
|
||||
<div class="form-text small">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</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">
|
||||
<i data-lucide="save" class="me-2 icon-sm"></i> Simpan Supplier
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card glass-card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0 d-flex align-items-center">
|
||||
<div class="bg-primary-subtle text-primary p-2 rounded-3 me-3">
|
||||
<i data-lucide="truck"></i>
|
||||
</div>
|
||||
Daftar Supplier
|
||||
</h4>
|
||||
<span class="badge bg-light text-dark border px-3 py-2 rounded-pill">{{ suppliers|length }} Total</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nama Supplier</th>
|
||||
<th>Kontak</th>
|
||||
<th>Telepon</th>
|
||||
<th class="text-end">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in suppliers %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold text-dark">{{ s.name }}</div>
|
||||
<div class="text-muted small">{{ s.address|truncatechars:40 }}</div>
|
||||
</td>
|
||||
<td>{{ s.contact_person|default:"-" }}</td>
|
||||
<td><span class="badge bg-light text-primary fw-medium">{{ s.phone|default:"-" }}</span></td>
|
||||
<td class="text-end">
|
||||
<a href="/admin/core/supplier/{{ s.id }}/change/" class="btn btn-light btn-sm rounded-circle p-2">
|
||||
<i data-lucide="edit-3" class="icon-sm"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-5 text-muted">
|
||||
<i data-lucide="inbox" class="mb-2 d-block mx-auto opacity-20" style="width: 48px; height: 48px;"></i>
|
||||
Belum ada data supplier.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
69
core/templates/registration/login.html
Normal file
69
core/templates/registration/login.html
Normal file
@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center align-items-center" style="min-height: 70vh;">
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="card glass-card border-0 shadow-lg p-2">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<div class="text-center mb-5">
|
||||
<div class="bg-primary text-white rounded-4 d-inline-flex align-items-center justify-content-center mb-3" style="width: 64px; height: 64px; box-shadow: 0 10px 20px rgba(67, 97, 238, 0.3);">
|
||||
<i data-lucide="shield-check" size="32"></i>
|
||||
</div>
|
||||
<h2 class="fw-bold text-dark">{{ project_name|default:"DN-WRS" }}</h2>
|
||||
<p class="text-muted small">Masukkan kredensial Anda untuk mengakses portal</p>
|
||||
</div>
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger border-0 small py-3 rounded-4 d-flex align-items-center mb-4">
|
||||
<i data-lucide="alert-circle" class="me-2" size="18"></i>
|
||||
<span>Username atau password salah.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="id_username" class="form-label small fw-bold text-uppercase opacity-50">Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-light rounded-start-3">
|
||||
<i data-lucide="user" size="18" class="text-muted"></i>
|
||||
</span>
|
||||
<input type="text" name="username" autofocus maxlength="150" required id="id_username" class="form-control rounded-end-3 py-2 border-light bg-light">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="id_password" class="form-label small fw-bold text-uppercase opacity-50">Password</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-light rounded-start-3">
|
||||
<i data-lucide="lock" size="18" class="text-muted"></i>
|
||||
</span>
|
||||
<input type="password" name="password" required id="id_password" class="form-control rounded-end-3 py-2 border-light bg-light">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid pt-2">
|
||||
<button type="submit" class="btn btn-primary py-3 rounded-3 d-flex align-items-center justify-content-center">
|
||||
<span>Masuk</span>
|
||||
<i data-lucide="arrow-right" class="ms-2" size="18"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4 opacity-50">
|
||||
<small>Diproteksi oleh DN-Security</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: radial-gradient(circle at 0% 0%, rgba(67, 97, 238, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 100% 100%, rgba(114, 9, 183, 0.05) 0%, transparent 50%),
|
||||
linear-gradient(135deg, #f6f8fd 0%, #f1f4f9 100%);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
16
core/urls.py
16
core/urls.py
@ -1,7 +1,17 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path('', views.home, name='home'),
|
||||
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'),
|
||||
path('categories/', views.category_list, name='category_list'),
|
||||
path('faktur/input/', views.input_faktur, name='input_faktur'),
|
||||
path('faktur/<int:pk>/', views.faktur_detail, name='faktur_detail'),
|
||||
path('barang-keluar/', views.barang_keluar, name='barang_keluar'),
|
||||
path('api/batches/', views.get_batches, name='get_batches'),
|
||||
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'),
|
||||
]
|
||||
|
||||
398
core/views.py
398
core/views.py
@ -1,25 +1,403 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
import io
|
||||
from datetime import timedelta
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.db.models import Sum, Q
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from .models import Medicine, Batch, Category, StockTransaction, Supplier, Faktur
|
||||
from .forms import SupplierForm, FakturForm, StockInForm, StockOutForm, CategoryForm, MedicineForm
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
"""Render the medicine warehouse dashboard."""
|
||||
now = timezone.now()
|
||||
today = now.date()
|
||||
|
||||
# Stats
|
||||
total_medicines = Medicine.objects.count()
|
||||
|
||||
# Total stock across all batches
|
||||
total_stock = Batch.objects.aggregate(total=Sum('quantity'))['total'] or 0
|
||||
|
||||
# Expired batches
|
||||
expired_batches_count = Batch.objects.filter(expiry_date__lte=today).count()
|
||||
|
||||
# Near expiry (next 90 days)
|
||||
near_expiry_batches_count = Batch.objects.filter(
|
||||
expiry_date__gt=today,
|
||||
expiry_date__lte=today + timezone.timedelta(days=90)
|
||||
).count()
|
||||
|
||||
# Low stock medicines
|
||||
all_medicines = Medicine.objects.all()
|
||||
low_stock_count = 0
|
||||
for med in all_medicines:
|
||||
if med.total_stock <= med.min_stock:
|
||||
low_stock_count += 1
|
||||
|
||||
# Latest medicines for the table
|
||||
recent_medicines = all_medicines.order_by('-created_at')[:5]
|
||||
|
||||
# Chart Data: Last 7 days movement
|
||||
chart_labels = []
|
||||
chart_data_in = []
|
||||
chart_data_out = []
|
||||
|
||||
for i in range(6, -1, -1):
|
||||
day = today - timedelta(days=i)
|
||||
chart_labels.append(day.strftime('%d %b'))
|
||||
|
||||
in_sum = StockTransaction.objects.filter(
|
||||
transaction_type='IN',
|
||||
created_at__date=day
|
||||
).aggregate(total=Sum('quantity'))['total'] or 0
|
||||
|
||||
out_sum = StockTransaction.objects.filter(
|
||||
transaction_type='OUT',
|
||||
created_at__date=day
|
||||
).aggregate(total=Sum('quantity'))['total'] or 0
|
||||
|
||||
chart_data_in.append(in_sum)
|
||||
chart_data_out.append(out_sum)
|
||||
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"project_name": "DN-WRS",
|
||||
"total_medicines": total_medicines,
|
||||
"total_stock": total_stock,
|
||||
"expired_count": expired_batches_count,
|
||||
"near_expiry_count": near_expiry_batches_count,
|
||||
"low_stock_count": low_stock_count,
|
||||
"recent_medicines": recent_medicines,
|
||||
"chart_labels": chart_labels,
|
||||
"chart_data_in": chart_data_in,
|
||||
"chart_data_out": chart_data_out,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
# --- MASTER DATA MANAGEMENT ---
|
||||
|
||||
@login_required
|
||||
def category_list(request):
|
||||
if request.method == 'POST':
|
||||
form = CategoryForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Kategori berhasil ditambahkan.")
|
||||
return redirect('category_list')
|
||||
else:
|
||||
form = CategoryForm()
|
||||
|
||||
categories = Category.objects.all().order_by('name')
|
||||
return render(request, 'core/categories.html', {
|
||||
'categories': categories,
|
||||
'form': form,
|
||||
'project_name': 'DN-WRS'
|
||||
})
|
||||
|
||||
@login_required
|
||||
def supplier_list(request):
|
||||
if request.method == 'POST':
|
||||
form = SupplierForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Supplier berhasil ditambahkan.")
|
||||
return redirect('supplier_list')
|
||||
else:
|
||||
form = SupplierForm()
|
||||
|
||||
suppliers = Supplier.objects.all().order_by('name')
|
||||
return render(request, 'core/suppliers.html', {
|
||||
'suppliers': suppliers,
|
||||
'form': form,
|
||||
'project_name': 'DN-WRS'
|
||||
})
|
||||
|
||||
@login_required
|
||||
def medicine_list(request):
|
||||
query = request.GET.get('q')
|
||||
show_low_stock = request.GET.get('low_stock') == '1'
|
||||
|
||||
if request.method == 'POST':
|
||||
form = MedicineForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Data barang berhasil ditambahkan.")
|
||||
return redirect('medicine_list')
|
||||
else:
|
||||
form = MedicineForm()
|
||||
|
||||
medicines_all = Medicine.objects.all().order_by('name')
|
||||
|
||||
# Calculate low stock count for the summary
|
||||
low_stock_count = 0
|
||||
for med in medicines_all:
|
||||
if med.total_stock <= med.min_stock:
|
||||
low_stock_count += 1
|
||||
|
||||
medicines = medicines_all
|
||||
|
||||
if query:
|
||||
medicines = medicines.filter(
|
||||
Q(name__icontains=query) |
|
||||
Q(sku__icontains=query) |
|
||||
Q(category__name__icontains=query)
|
||||
)
|
||||
|
||||
if show_low_stock:
|
||||
# Filter for low stock
|
||||
low_stock_ids = [m.id for m in medicines if m.total_stock <= m.min_stock]
|
||||
medicines = medicines.filter(id__in=low_stock_ids)
|
||||
|
||||
return render(request, 'core/medicines.html', {
|
||||
'medicines': medicines,
|
||||
'form': form,
|
||||
'project_name': 'DN-WRS',
|
||||
'query': query,
|
||||
'show_low_stock': show_low_stock,
|
||||
'low_stock_count': low_stock_count,
|
||||
'total_count': medicines_all.count()
|
||||
})
|
||||
|
||||
@login_required
|
||||
def export_low_stock_pdf(request):
|
||||
# Get low stock medicines
|
||||
all_medicines = Medicine.objects.all().order_by('main_supplier__name', 'name')
|
||||
low_stock_medicines = [m for m in all_medicines if m.total_stock <= m.min_stock]
|
||||
|
||||
# Group by main supplier
|
||||
grouped = {}
|
||||
for m in low_stock_medicines:
|
||||
supplier_name = m.main_supplier.name if m.main_supplier else "Tanpa Supplier Utama"
|
||||
if supplier_name not in grouped:
|
||||
grouped[supplier_name] = []
|
||||
grouped[supplier_name].append(m)
|
||||
|
||||
# Generate PDF
|
||||
buffer = io.BytesIO()
|
||||
doc = SimpleDocTemplate(buffer, pagesize=A4)
|
||||
elements = []
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
title_style = ParagraphStyle(
|
||||
'Title',
|
||||
parent=styles['Heading1'],
|
||||
fontSize=18,
|
||||
alignment=1,
|
||||
spaceAfter=20,
|
||||
textColor=colors.HexColor("#0d6efd")
|
||||
)
|
||||
|
||||
elements.append(Paragraph("Laporan Daftar Stok Menipis", title_style))
|
||||
elements.append(Paragraph(f"Tanggal Cetak: {timezone.now().strftime('%d %B %Y %H:%M')}", styles["Normal"]))
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
if not low_stock_medicines:
|
||||
elements.append(Paragraph("Tidak ada stok yang menipis saat ini.", styles["Normal"]))
|
||||
else:
|
||||
for supplier, items in grouped.items():
|
||||
elements.append(Paragraph(f"Supplier Utama: {supplier}", styles["Heading2"]))
|
||||
elements.append(Spacer(1, 5))
|
||||
|
||||
data = [["Nama Barang", "SKU", "Stok", "Min.", "Satuan", "Supplier Alternatif"]]
|
||||
for item in items:
|
||||
alt_supplier = item.alternative_supplier.name if item.alternative_supplier else "-"
|
||||
data.append([
|
||||
item.name,
|
||||
item.sku,
|
||||
str(item.total_stock),
|
||||
str(item.min_stock),
|
||||
item.unit,
|
||||
alt_supplier
|
||||
])
|
||||
|
||||
table = Table(data, colWidths=[160, 60, 40, 40, 50, 110])
|
||||
table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#343a40")),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 10),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
||||
('TOPPADDING', (0, 0), (-1, 0), 10),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 9),
|
||||
]))
|
||||
elements.append(table)
|
||||
elements.append(Spacer(1, 15))
|
||||
|
||||
doc.build(elements)
|
||||
|
||||
buffer.seek(0)
|
||||
response = HttpResponse(buffer, content_type='application/pdf')
|
||||
response['Content-Disposition'] = 'attachment; filename="laporan_stok_menipis.pdf"'
|
||||
return response
|
||||
|
||||
# --- TRANSAKSI ---
|
||||
|
||||
@login_required
|
||||
def input_faktur(request):
|
||||
if request.method == 'POST':
|
||||
faktur_form = FakturForm(request.POST)
|
||||
if faktur_form.is_valid():
|
||||
faktur = faktur_form.save()
|
||||
return redirect('faktur_detail', pk=faktur.pk)
|
||||
else:
|
||||
faktur_form = FakturForm(initial={'faktur_type': 'MASUK', 'date': timezone.now().date()})
|
||||
|
||||
fakturs = Faktur.objects.all().order_by('-created_at')
|
||||
return render(request, 'core/input_faktur.html', {
|
||||
'faktur_form': faktur_form,
|
||||
'fakturs': fakturs,
|
||||
'project_name': 'DN-WRS'
|
||||
})
|
||||
|
||||
@login_required
|
||||
def faktur_detail(request, pk):
|
||||
faktur = get_object_or_404(Faktur, pk=pk)
|
||||
if request.method == 'POST':
|
||||
form = StockInForm(request.POST)
|
||||
if form.is_valid():
|
||||
# Create Batch
|
||||
batch = Batch.objects.create(
|
||||
medicine=form.cleaned_data['medicine'],
|
||||
faktur=faktur,
|
||||
batch_number=form.cleaned_data['batch_number'],
|
||||
expiry_date=form.cleaned_data['expiry_date'],
|
||||
quantity=form.cleaned_data['quantity'],
|
||||
buying_price=form.cleaned_data['buying_price'],
|
||||
selling_price=form.cleaned_data['selling_price']
|
||||
)
|
||||
# Create Transaction
|
||||
StockTransaction.objects.create(
|
||||
medicine=batch.medicine,
|
||||
batch=batch,
|
||||
faktur=faktur,
|
||||
transaction_type='IN',
|
||||
quantity=batch.quantity,
|
||||
note=f"Input dari Faktur {faktur.faktur_number}"
|
||||
)
|
||||
messages.success(request, f"Barang {batch.medicine.name} berhasil ditambahkan.")
|
||||
return redirect('faktur_detail', pk=pk)
|
||||
else:
|
||||
form = StockInForm()
|
||||
|
||||
items = Batch.objects.filter(faktur=faktur)
|
||||
return render(request, 'core/faktur_detail.html', {
|
||||
'faktur': faktur,
|
||||
'form': form,
|
||||
'items': items,
|
||||
'project_name': 'DN-WRS'
|
||||
})
|
||||
|
||||
@login_required
|
||||
def barang_keluar(request):
|
||||
if request.method == 'POST':
|
||||
form = StockOutForm(request.POST)
|
||||
if form.is_valid():
|
||||
medicine = form.cleaned_data['medicine']
|
||||
batch = form.cleaned_data['batch']
|
||||
qty = form.cleaned_data['quantity']
|
||||
|
||||
if batch.quantity < qty:
|
||||
messages.error(request, f"Stok tidak mencukupi. Stok saat ini: {batch.quantity}")
|
||||
else:
|
||||
# Update Batch
|
||||
batch.quantity -= qty
|
||||
batch.save()
|
||||
|
||||
# Create Transaction
|
||||
StockTransaction.objects.create(
|
||||
medicine=medicine,
|
||||
batch=batch,
|
||||
transaction_type='OUT',
|
||||
quantity=qty,
|
||||
note=form.cleaned_data['note']
|
||||
)
|
||||
messages.success(request, f"Barang keluar berhasil dicatat.")
|
||||
return redirect('barang_keluar')
|
||||
else:
|
||||
form = StockOutForm()
|
||||
|
||||
transactions = StockTransaction.objects.filter(transaction_type='OUT').order_by('-created_at')[:10]
|
||||
return render(request, 'core/barang_keluar.html', {
|
||||
'form': form,
|
||||
'transactions': transactions,
|
||||
'project_name': 'DN-WRS'
|
||||
})
|
||||
|
||||
@login_required
|
||||
def get_batches(request):
|
||||
medicine_id = request.GET.get('medicine_id')
|
||||
batches = Batch.objects.filter(medicine_id=medicine_id, quantity__gt=0).values('id', 'batch_number', 'quantity')
|
||||
return JsonResponse(list(batches), safe=False)
|
||||
|
||||
@login_required
|
||||
def laporan_transaksi(request):
|
||||
transactions = StockTransaction.objects.all().order_by('-created_at')
|
||||
return render(request, 'core/laporan_transaksi.html', {
|
||||
'transactions': transactions,
|
||||
'project_name': 'DN-WRS'
|
||||
})
|
||||
|
||||
@login_required
|
||||
def delete_transaksi(request, pk):
|
||||
transaction = get_object_or_404(StockTransaction, pk=pk)
|
||||
batch = transaction.batch
|
||||
|
||||
if batch:
|
||||
if transaction.transaction_type == 'IN':
|
||||
batch.quantity -= transaction.quantity
|
||||
elif transaction.transaction_type == 'OUT':
|
||||
batch.quantity += transaction.quantity
|
||||
batch.save()
|
||||
|
||||
transaction.delete()
|
||||
messages.success(request, "Transaksi berhasil dihapus dan stok telah diperbarui.")
|
||||
return redirect('laporan_transaksi')
|
||||
|
||||
@login_required
|
||||
def edit_transaksi(request, pk):
|
||||
transaction = get_object_or_404(StockTransaction, pk=pk)
|
||||
if request.method == 'POST':
|
||||
new_qty = int(request.POST.get('quantity'))
|
||||
old_qty = transaction.quantity
|
||||
batch = transaction.batch
|
||||
|
||||
if batch:
|
||||
if transaction.transaction_type == 'IN':
|
||||
# Revert old, apply new
|
||||
batch.quantity = batch.quantity - old_qty + new_qty
|
||||
elif transaction.transaction_type == 'OUT':
|
||||
# Revert old, apply new
|
||||
batch.quantity = batch.quantity + old_qty - new_qty
|
||||
|
||||
if batch.quantity < 0:
|
||||
messages.error(request, "Error: Stok tidak boleh negatif setelah perubahan.")
|
||||
return redirect('laporan_transaksi')
|
||||
|
||||
batch.save()
|
||||
|
||||
transaction.quantity = new_qty
|
||||
transaction.note = request.POST.get('note', transaction.note)
|
||||
transaction.save()
|
||||
messages.success(request, "Transaksi berhasil diperbarui.")
|
||||
return redirect('laporan_transaksi')
|
||||
|
||||
return render(request, 'core/edit_transaksi.html', {
|
||||
'transaction': transaction,
|
||||
'project_name': 'DN-WRS'
|
||||
})
|
||||
@ -1,9 +1,9 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_NAME', 'app_38226');
|
||||
define('DB_USER', 'app_38226');
|
||||
define('DB_PASS', 'c892d620-ef33-4130-848d-608166951927');
|
||||
define('DB_NAME', 'app_38240');
|
||||
define('DB_USER', 'app_38240');
|
||||
define('DB_PASS', '0f262299-4701-4e42-af5e-1a1136825b7a');
|
||||
|
||||
function db() {
|
||||
static $pdo;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
reportlab==4.1.0
|
||||
@ -1,4 +1,122 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
/* Modern UI Styles for DN-WRS */
|
||||
|
||||
:root {
|
||||
--primary: #4361ee;
|
||||
--primary-hover: #3a56d4;
|
||||
--secondary: #7209b7;
|
||||
--success: #4cc9f0;
|
||||
--info: #4895ef;
|
||||
--warning: #f72585;
|
||||
--danger: #ef233c;
|
||||
--dark: #2b2d42;
|
||||
--light: #f8f9fa;
|
||||
--glass: rgba(255, 255, 255, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background: linear-gradient(135deg, #f6f8fd 0%, #f1f4f9 100%);
|
||||
background-attachment: fixed;
|
||||
color: var(--dark);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Glassmorphism */
|
||||
.glass-card {
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07);
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 1.25rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.navbar-custom {
|
||||
background: rgba(255, 255, 255, 0.8) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
box-shadow: 0 4px 14px 0 rgba(67, 97, 238, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 6px 20px rgba(67, 97, 238, 0.4);
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.5rem;
|
||||
}
|
||||
|
||||
.table tr {
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.table td, .table th {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 1px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Stats Cards */
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user