diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 423a636..1ef46bf 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..895ce6e 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..8275db0 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 9c49e09..f9b82d3 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..e9dc34b 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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/' diff --git a/config/urls.py b/config/urls.py index bcfc074..fecf250 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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) \ No newline at end of file diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 74b1112..ff36288 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..7f66d9b 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6f131d4..a1495c5 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..86948bc 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..74fd730 Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..a72c45c 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..b11b471 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..1004ca8 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..7d50940 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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') \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..ec37e48 --- /dev/null +++ b/core/forms.py @@ -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) \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..0f1f7bf --- /dev/null +++ b/core/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/core/migrations/0002_faktur_supplier_alter_batch_options_and_more.py b/core/migrations/0002_faktur_supplier_alter_batch_options_and_more.py new file mode 100644 index 0000000..9a6c1a0 --- /dev/null +++ b/core/migrations/0002_faktur_supplier_alter_batch_options_and_more.py @@ -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'), + ), + ] diff --git a/core/migrations/0003_medicine_alternative_supplier_medicine_main_supplier.py b/core/migrations/0003_medicine_alternative_supplier_medicine_main_supplier.py new file mode 100644 index 0000000..b7b0efa --- /dev/null +++ b/core/migrations/0003_medicine_alternative_supplier_medicine_main_supplier.py @@ -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'), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..d57b125 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_faktur_supplier_alter_batch_options_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_faktur_supplier_alter_batch_options_and_more.cpython-311.pyc new file mode 100644 index 0000000..bf00305 Binary files /dev/null and b/core/migrations/__pycache__/0002_faktur_supplier_alter_batch_options_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_medicine_alternative_supplier_medicine_main_supplier.cpython-311.pyc b/core/migrations/__pycache__/0003_medicine_alternative_supplier_medicine_main_supplier.cpython-311.pyc new file mode 100644 index 0000000..9e2b897 Binary files /dev/null and b/core/migrations/__pycache__/0003_medicine_alternative_supplier_medicine_main_supplier.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 9c833c8..d364bdb 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..324aa1c 100644 --- a/core/models.py +++ b/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" \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..cecdb8a 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,128 @@ +{% load static %} - - + - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {{ project_name|default:"DN-WRS" }} + + + + + + + + + + + + + + {% block extra_head %}{% endblock %} - - {% block content %}{% endblock %} - + {% if user.is_authenticated %} + + {% endif %} - +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} +
+ +
+ {% block content %}{% endblock %} +
+ + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/barang_keluar.html b/core/templates/core/barang_keluar.html new file mode 100644 index 0000000..5da9a0f --- /dev/null +++ b/core/templates/core/barang_keluar.html @@ -0,0 +1,152 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+
+

+
+ +
+ Barang Keluar +

+
+ {% csrf_token %} +
+ + {{ form.medicine }} +
+
+ + {{ form.batch }} +
+
+
+ + {{ form.quantity }} +
+
+ + {{ form.note }} +
+ +
+
+
+
+
+
+
+
+

+
+ +
+ Transaksi Terakhir +

+ 10 Transaksi +
+
+ + + + + + + + + + + + {% for t in transactions %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
BarangBatchJumlahWaktuAksi
+
{{ t.medicine.name }}
+
{{ t.note|default:"-" }}
+
{{ t.batch.batch_number }}-{{ t.quantity }}{{ t.created_at|date:"d/m/y H:i" }} + + + +
+ + Belum ada transaksi keluar. +
+
+
+
+
+
+{% endblock %} + +{% block extra_head %} + + +{% endblock %} + +{% block extra_js %} + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/categories.html b/core/templates/core/categories.html new file mode 100644 index 0000000..706660c --- /dev/null +++ b/core/templates/core/categories.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+
+

+
+ +
+ Tambah Kategori +

+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+
+
+
+
+
+
+
+

+
+ +
+ Daftar Kategori +

+ {{ categories|length }} Total +
+
+ + + + + + + + + + {% for c in categories %} + + + + + + {% empty %} + + + + {% endfor %} + +
Nama KategoriDeskripsiAksi
+
{{ c.name }}
+
{{ c.description|default:"-"|truncatechars:100 }} + + + +
+ + Belum ada data kategori. +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/edit_transaksi.html b/core/templates/core/edit_transaksi.html new file mode 100644 index 0000000..ce63011 --- /dev/null +++ b/core/templates/core/edit_transaksi.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+
+ + +

+
+ +
+ Edit Transaksi +

+ +
+
+
+ BARANG + {{ transaction.medicine.name }} +
+
+ TIPE + + {{ transaction.get_transaction_type_display }} + +
+
+ BATCH NUMBER + {{ transaction.batch.batch_number }} +
+
+
+ +
+ {% csrf_token %} +
+ + +
Perubahan jumlah akan otomatis memperbarui stok pada batch.
+
+
+ + +
+ +
+ Batal + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/faktur_detail.html b/core/templates/core/faktur_detail.html new file mode 100644 index 0000000..4e1a61d --- /dev/null +++ b/core/templates/core/faktur_detail.html @@ -0,0 +1,131 @@ +{% extends 'base.html' %} +{% block content %} +
+ +
+
+
+
NOMOR FAKTUR
+

#{{ faktur.faktur_number }}

+
+
+
SUPPLIER
+
+ {{ faktur.supplier.name|default:"Internal" }} +
+
+
+
+
+ +
+
+
+
+
+
+ +
+ Input Barang +
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endfor %} + +
+
+
+
+
+
+
+
+
+
+ +
+ Rincian Barang +
+ {{ items|length }} Item +
+
+ + + + + + + + + + + + {% for item in items %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
BarangNo. BatchKadaluarsaJumlahHarga Beli
+
{{ item.medicine.name }}
+
{{ item.medicine.sku }}
+
{{ item.batch_number }}{{ item.expiry_date|date:"d M Y" }}{{ item.quantity }} {{ item.medicine.unit }}Rp {{ item.buying_price|floatformat:2 }}
+ +

Belum ada barang diinput untuk faktur ini.

+
+
+
+
+
+
+{% endblock %} +{% block extra_head %} + + + +{% endblock %} +{% block extra_js %} + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..6109bca 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,260 @@ {% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% load static %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+ +
+
+
+
+ +
+
TOTAL BARANG
+

{{ total_medicines }}

+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file +
+
+
+
+ +
+
TOTAL STOK
+

{{ total_stock }}

+
+
+
+
+
+
+
+ +
+
STOK MENIPIS
+

{{ low_stock_count }}

+
+
+
+
+
+
+
+ +
+
KADALUARSA
+

{{ expired_count }}

+
+
+
+ + +
+ +
+
+
+
+
Pergerakan Stok
+ 7 Hari Terakhir +
+
+ +
+
+
+
+ + +
+
+
+
+
Pantauan Kadaluarsa
+

Segera kadaluarsa (90 hari ke depan)

+
+
+
{{ near_expiry_count }}
+

Item memerlukan perhatian segera

+ +
+
+
+
+
+ +
+
+
+
+
+
+ Data Barang Terbaru +
+ + Kelola Semua + +
+
+ + + + + + + + + + + + + {% for medicine in recent_medicines %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
SKUNama BarangKategoriStokStatusAksi
{{ medicine.sku }}{{ medicine.name }}{{ medicine.category.name }} + {{ medicine.total_stock }} + {{ medicine.unit }} + + {% if medicine.status == 'Tersedia' %} + Tersedia + {% elif medicine.status == 'Stok Menipis' %} + Stok Menipis + {% else %} + Habis + {% endif %} + + + + +
+ + Belum ada data tersedia. +
+
+
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/core/templates/core/input_faktur.html b/core/templates/core/input_faktur.html new file mode 100644 index 0000000..b8e874b --- /dev/null +++ b/core/templates/core/input_faktur.html @@ -0,0 +1,86 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+
+

+
+ +
+ Input Faktur Baru +

+
+ {% csrf_token %} + {% for field in faktur_form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endfor %} + +
+
+
+
+
+
+
+
+

+
+ +
+ Riwayat Faktur +

+ {{ fakturs|length }} Total +
+
+ + + + + + + + + + + + {% for f in fakturs %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
No. FakturSupplierTanggalTipeAksi
{{ f.faktur_number }}{{ f.supplier.name|default:"-" }}{{ f.date }} + + {{ f.get_faktur_type_display }} + + + + Input Barang + +
+ + Belum ada data faktur. +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/laporan_transaksi.html b/core/templates/core/laporan_transaksi.html new file mode 100644 index 0000000..b38b49b --- /dev/null +++ b/core/templates/core/laporan_transaksi.html @@ -0,0 +1,122 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+

+
+ +
+ Laporan Keluar Masuk Barang +

+
+ +
+
+ +
+ + + + + + + + + + + + + {% for t in transactions %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
WaktuBarangTipeJumlahKeteranganAksi
+
{{ t.created_at|date:"d M Y" }}
+
{{ t.created_at|date:"H:i" }} WIB
+
+
{{ t.medicine.name }}
+
Batch: {{ t.batch.batch_number|default:"-" }}
+
+ {% if t.transaction_type == 'IN' %} + Masuk + {% elif t.transaction_type == 'OUT' %} + Keluar + {% else %} + Penyesuaian + {% endif %} + + {% if t.transaction_type == 'OUT' %} + -{{ t.quantity }} + {% else %} + +{{ t.quantity }} + {% endif %} + {{ t.note|default:"-" }} +
+ + + + +
+
+ + Belum ada transaksi stok. +
+
+
+
+ + + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/medicines.html b/core/templates/core/medicines.html new file mode 100644 index 0000000..612fd9c --- /dev/null +++ b/core/templates/core/medicines.html @@ -0,0 +1,238 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+
+
+ +
+
Total Barang
+

{{ total_count }}

+
+
+
+
+
+
+
+ +
+
Stok Menipis
+

{{ low_stock_count }}

+
+
+
+
+
+
+
+ +
+
Status Aman
+

{{ total_count|add:"-"|add:low_stock_count|default:0|cut:"-" }}

+
+
+
+
+ +
+
+
+

+
+ +
+ Manajemen Data Barang +

+
+ + + Export Stok Menipis (PDF) + +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + {% for m in medicines %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
SKUNama BarangKategoriSupplier UtamaSupplier AlternatifStokStatusAksi
{{ m.sku }} +
{{ m.name }}
+
+ {{ m.unit }} +
+
+ {{ m.category.name }} + +
{{ m.main_supplier.name|default:"-" }}
+ {% if m.main_supplier %} +
{{ m.main_supplier.phone|default:"N/A" }}
+ {% endif %} +
+
{{ m.alternative_supplier.name|default:"-" }}
+
+
+ {{ m.total_stock }} +
+
Min: {{ m.min_stock }}
+
+ {% if m.status == 'Tersedia' %} + + Tersedia + + {% elif m.status == 'Stok Menipis' %} + + Menipis + + {% else %} + + Habis + + {% endif %} + + +
+
+ +
Tidak ada data ditemukan
+

Coba ubah kata kunci pencarian atau filter Anda.

+ Reset Semua Filter +
+
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/suppliers.html b/core/templates/core/suppliers.html new file mode 100644 index 0000000..027c603 --- /dev/null +++ b/core/templates/core/suppliers.html @@ -0,0 +1,86 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+
+

+
+ +
+ Tambah Supplier +

+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+
+
+
+
+
+
+
+

+
+ +
+ Daftar Supplier +

+ {{ suppliers|length }} Total +
+
+ + + + + + + + + + + {% for s in suppliers %} + + + + + + + {% empty %} + + + + {% endfor %} + +
Nama SupplierKontakTeleponAksi
+
{{ s.name }}
+
{{ s.address|truncatechars:40 }}
+
{{ s.contact_person|default:"-" }}{{ s.phone|default:"-" }} + + + +
+ + Belum ada data supplier. +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..c742327 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+
+
+
+
+ +
+

{{ project_name|default:"DN-WRS" }}

+

Masukkan kredensial Anda untuk mengakses portal

+
+ + {% if form.errors %} +
+ + Username atau password salah. +
+ {% endif %} + +
+ {% csrf_token %} +
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+ +
+ +
+ + +
+
+
+
+ Diproteksi oleh DN-Security +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..898bcdc 100644 --- a/core/urls.py +++ b/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//', 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//delete/', views.delete_transaksi, name='delete_transaksi'), + path('transaksi//edit/', views.edit_transaksi, name='edit_transaksi'), ] diff --git a/core/views.py b/core/views.py index c9aed12..2f67b54 100644 --- a/core/views.py +++ b/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' + }) \ No newline at end of file diff --git a/db/config.php b/db/config.php index 1167971..777c778 100644 --- a/db/config.php +++ b/db/config.php @@ -1,9 +1,9 @@