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 %} - - +
- -