first commit
This commit is contained in:
commit
c001cd97fc
20
.env
Normal file
20
.env
Normal file
@ -0,0 +1,20 @@
|
||||
# Django Settings
|
||||
DEBUG=True
|
||||
SECRET_KEY=your-development-secret-key
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
|
||||
# Database Settings (SQLite for development)
|
||||
# For PostgreSQL in production, uncomment and set these:
|
||||
# DATABASE_URL=postgres://user:password@host:port/dbname
|
||||
# DB_NAME=your_db_name
|
||||
# DB_USER=your_db_user
|
||||
# DB_PASSWORD=your_db_password
|
||||
# DB_HOST=your_db_host
|
||||
# DB_PORT=5432
|
||||
|
||||
# Email Settings (optional)
|
||||
# EMAIL_HOST=your_email_host
|
||||
# EMAIL_PORT=587
|
||||
# EMAIL_USE_TLS=True
|
||||
# EMAIL_HOST_USER=your_email_user
|
||||
# EMAIL_HOST_PASSWORD=your_email_password
|
||||
0
apps/accounts/__init__.py
Normal file
0
apps/accounts/__init__.py
Normal file
BIN
apps/accounts/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/accounts/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/admin.cpython-311.pyc
Normal file
BIN
apps/accounts/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/apps.cpython-311.pyc
Normal file
BIN
apps/accounts/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/context_processors.cpython-311.pyc
Normal file
BIN
apps/accounts/__pycache__/context_processors.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/models.cpython-311.pyc
Normal file
BIN
apps/accounts/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/urls.cpython-311.pyc
Normal file
BIN
apps/accounts/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/views.cpython-311.pyc
Normal file
BIN
apps/accounts/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
3
apps/accounts/admin.py
Normal file
3
apps/accounts/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
apps/accounts/apps.py
Normal file
6
apps/accounts/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.accounts"
|
||||
35
apps/accounts/context_processors.py
Normal file
35
apps/accounts/context_processors.py
Normal file
@ -0,0 +1,35 @@
|
||||
from .models import Role, Permission, RolePermission, UserRole
|
||||
|
||||
|
||||
def custom_permissions(request):
|
||||
"""
|
||||
Context processor to add custom role-based permissions to template context
|
||||
"""
|
||||
if not request.user.is_authenticated:
|
||||
return {}
|
||||
|
||||
# Get user's roles
|
||||
user_roles = UserRole.objects.filter(user=request.user).values_list('role', flat=True)
|
||||
|
||||
# Get all permissions for user's roles
|
||||
role_permissions = RolePermission.objects.filter(role__in=user_roles).select_related('permission')
|
||||
|
||||
# Create a dictionary of permissions organized by module
|
||||
user_permissions = {}
|
||||
for rp in role_permissions:
|
||||
permission = rp.permission
|
||||
module = permission.module
|
||||
codename = permission.codename
|
||||
|
||||
if module not in user_permissions:
|
||||
user_permissions[module] = {}
|
||||
|
||||
user_permissions[module][codename] = True
|
||||
|
||||
# Also add flat permissions for backward compatibility
|
||||
flat_permissions = {rp.permission.codename: True for rp in role_permissions}
|
||||
|
||||
return {
|
||||
'user_permissions': user_permissions,
|
||||
'user_flat_permissions': flat_permissions,
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,81 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from apps.accounts.models import Permission
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Create default permissions for all modules'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
default_permissions = [
|
||||
# Dashboard permissions
|
||||
{'name': 'View Dashboard', 'codename': 'view_dashboard', 'module': 'dashboard', 'description': 'Can view dashboard'},
|
||||
|
||||
# User management permissions
|
||||
{'name': 'View User', 'codename': 'view_user', 'module': 'accounts', 'description': 'Can view user list and details'},
|
||||
{'name': 'Add User', 'codename': 'add_user', 'module': 'accounts', 'description': 'Can create new users'},
|
||||
{'name': 'Change User', 'codename': 'change_user', 'module': 'accounts', 'description': 'Can edit user information'},
|
||||
{'name': 'Delete User', 'codename': 'delete_user', 'module': 'accounts', 'description': 'Can delete users'},
|
||||
|
||||
# Role management permissions
|
||||
{'name': 'View Role', 'codename': 'view_role', 'module': 'accounts', 'description': 'Can view role list and details'},
|
||||
{'name': 'Add Role', 'codename': 'add_role', 'module': 'accounts', 'description': 'Can create new roles'},
|
||||
{'name': 'Change Role', 'codename': 'change_role', 'module': 'accounts', 'description': 'Can edit role information'},
|
||||
{'name': 'Delete Role', 'codename': 'delete_role', 'module': 'accounts', 'description': 'Can delete roles'},
|
||||
|
||||
# Permission management permissions
|
||||
{'name': 'View Permission', 'codename': 'view_permission', 'module': 'accounts', 'description': 'Can view permission list'},
|
||||
{'name': 'Add Permission', 'codename': 'add_permission', 'module': 'accounts', 'description': 'Can create new permissions'},
|
||||
{'name': 'Change Permission', 'codename': 'change_permission', 'module': 'accounts', 'description': 'Can edit permissions'},
|
||||
{'name': 'Delete Permission', 'codename': 'delete_permission', 'module': 'accounts', 'description': 'Can delete permissions'},
|
||||
|
||||
# Inventory permissions
|
||||
{'name': 'View Product', 'codename': 'view_product', 'module': 'inventory', 'description': 'Can view product list and details'},
|
||||
{'name': 'Add Product', 'codename': 'add_product', 'module': 'inventory', 'description': 'Can create new products'},
|
||||
{'name': 'Change Product', 'codename': 'change_product', 'module': 'inventory', 'description': 'Can edit product information'},
|
||||
{'name': 'Delete Product', 'codename': 'delete_product', 'module': 'inventory', 'description': 'Can delete products'},
|
||||
|
||||
# Purchasing permissions
|
||||
{'name': 'View Purchase Order', 'codename': 'view_purchaseorder', 'module': 'purchasing', 'description': 'Can view purchase orders'},
|
||||
{'name': 'Add Purchase Order', 'codename': 'add_purchaseorder', 'module': 'purchasing', 'description': 'Can create purchase orders'},
|
||||
{'name': 'Change Purchase Order', 'codename': 'change_purchaseorder', 'module': 'purchasing', 'description': 'Can edit purchase orders'},
|
||||
{'name': 'Delete Purchase Order', 'codename': 'delete_purchaseorder', 'module': 'purchasing', 'description': 'Can delete purchase orders'},
|
||||
|
||||
# Sales permissions
|
||||
{'name': 'View Sales Order', 'codename': 'view_salesorder', 'module': 'sales', 'description': 'Can view sales orders'},
|
||||
{'name': 'Add Sales Order', 'codename': 'add_salesorder', 'module': 'sales', 'description': 'Can create sales orders'},
|
||||
{'name': 'Change Sales Order', 'codename': 'change_salesorder', 'module': 'sales', 'description': 'Can edit sales orders'},
|
||||
{'name': 'Delete Sales Order', 'codename': 'delete_salesorder', 'module': 'sales', 'description': 'Can delete sales orders'},
|
||||
|
||||
# Manufacturing permissions
|
||||
{'name': 'View Manufacturing Order', 'codename': 'view_manufacturingorder', 'module': 'manufacturing', 'description': 'Can view manufacturing orders'},
|
||||
{'name': 'Add Manufacturing Order', 'codename': 'add_manufacturingorder', 'module': 'manufacturing', 'description': 'Can create manufacturing orders'},
|
||||
{'name': 'Change Manufacturing Order', 'codename': 'change_manufacturingorder', 'module': 'manufacturing', 'description': 'Can edit manufacturing orders'},
|
||||
{'name': 'Delete Manufacturing Order', 'codename': 'delete_manufacturingorder', 'module': 'manufacturing', 'description': 'Can delete manufacturing orders'},
|
||||
|
||||
# Reports permissions
|
||||
{'name': 'View Report', 'codename': 'view_report', 'module': 'reports', 'description': 'Can view reports'},
|
||||
{'name': 'Add Report', 'codename': 'add_report', 'module': 'reports', 'description': 'Can create reports'},
|
||||
{'name': 'Change Report', 'codename': 'change_report', 'module': 'reports', 'description': 'Can edit reports'},
|
||||
{'name': 'Delete Report', 'codename': 'delete_report', 'module': 'reports', 'description': 'Can delete reports'},
|
||||
|
||||
# Database management permissions
|
||||
{'name': 'View Database', 'codename': 'view_database', 'module': 'database_management', 'description': 'Can view database management'},
|
||||
{'name': 'Add Database', 'codename': 'add_database', 'module': 'database_management', 'description': 'Can create database entries'},
|
||||
{'name': 'Change Database', 'codename': 'change_database', 'module': 'database_management', 'description': 'Can edit database entries'},
|
||||
{'name': 'Delete Database', 'codename': 'delete_database', 'module': 'database_management', 'description': 'Can delete database entries'},
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
for perm_data in default_permissions:
|
||||
permission, created = Permission.objects.get_or_create(
|
||||
codename=perm_data['codename'],
|
||||
defaults=perm_data
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'Successfully created {created_count} default permissions'
|
||||
)
|
||||
)
|
||||
221
apps/accounts/migrations/0001_initial.py
Normal file
221
apps/accounts/migrations/0001_initial.py
Normal file
@ -0,0 +1,221 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 02:29
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
("phone", models.CharField(blank=True, max_length=20)),
|
||||
("department", models.CharField(blank=True, max_length=100)),
|
||||
("position", models.CharField(blank=True, max_length=100)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("date_joined", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Permission",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100, unique=True)),
|
||||
("codename", models.CharField(max_length=100, unique=True)),
|
||||
("module", models.CharField(max_length=50)),
|
||||
("description", models.TextField(blank=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Role",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=50, unique=True)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserRole",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("assigned_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"role",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="accounts.role"
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("user", "role")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RolePermission",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"permission",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="accounts.permission",
|
||||
),
|
||||
),
|
||||
(
|
||||
"role",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="accounts.role"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("role", "permission")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 15:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="rolepermission",
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
0
apps/accounts/migrations/__init__.py
Normal file
0
apps/accounts/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/accounts/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/accounts/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
118
apps/accounts/models.py
Normal file
118
apps/accounts/models.py
Normal file
@ -0,0 +1,118 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_migrate
|
||||
from django.dispatch import receiver
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
department = models.CharField(max_length=100, blank=True)
|
||||
position = models.CharField(max_length=100, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
date_joined = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class UserRole(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
||||
assigned_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'role')
|
||||
|
||||
|
||||
class Permission(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
codename = models.CharField(max_length=100, unique=True)
|
||||
module = models.CharField(max_length=50) # e.g., 'inventory', 'sales'
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
|
||||
class RolePermission(models.Model):
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
||||
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
@receiver(post_migrate)
|
||||
def create_default_permissions(sender, **kwargs):
|
||||
"""Create default permissions for all modules"""
|
||||
if sender.name == 'apps.accounts':
|
||||
# Define default permissions for each module
|
||||
default_permissions = [
|
||||
# Dashboard permissions
|
||||
{'name': 'View Dashboard', 'codename': 'view_dashboard', 'module': 'dashboard', 'description': 'Can view dashboard'},
|
||||
|
||||
# User management permissions
|
||||
{'name': 'View User', 'codename': 'view_user', 'module': 'accounts', 'description': 'Can view user list and details'},
|
||||
{'name': 'Add User', 'codename': 'add_user', 'module': 'accounts', 'description': 'Can create new users'},
|
||||
{'name': 'Change User', 'codename': 'change_user', 'module': 'accounts', 'description': 'Can edit user information'},
|
||||
{'name': 'Delete User', 'codename': 'delete_user', 'module': 'accounts', 'description': 'Can delete users'},
|
||||
|
||||
# Role management permissions
|
||||
{'name': 'View Role', 'codename': 'view_role', 'module': 'accounts', 'description': 'Can view role list and details'},
|
||||
{'name': 'Add Role', 'codename': 'add_role', 'module': 'accounts', 'description': 'Can create new roles'},
|
||||
{'name': 'Change Role', 'codename': 'change_role', 'module': 'accounts', 'description': 'Can edit role information'},
|
||||
{'name': 'Delete Role', 'codename': 'delete_role', 'module': 'accounts', 'description': 'Can delete roles'},
|
||||
|
||||
# Permission management permissions
|
||||
{'name': 'View Permission', 'codename': 'view_permission', 'module': 'accounts', 'description': 'Can view permission list'},
|
||||
{'name': 'Add Permission', 'codename': 'add_permission', 'module': 'accounts', 'description': 'Can create new permissions'},
|
||||
{'name': 'Change Permission', 'codename': 'change_permission', 'module': 'accounts', 'description': 'Can edit permissions'},
|
||||
{'name': 'Delete Permission', 'codename': 'delete_permission', 'module': 'accounts', 'description': 'Can delete permissions'},
|
||||
|
||||
# Inventory permissions
|
||||
{'name': 'View Product', 'codename': 'view_product', 'module': 'inventory', 'description': 'Can view product list and details'},
|
||||
{'name': 'Add Product', 'codename': 'add_product', 'module': 'inventory', 'description': 'Can create new products'},
|
||||
{'name': 'Change Product', 'codename': 'change_product', 'module': 'inventory', 'description': 'Can edit product information'},
|
||||
{'name': 'Delete Product', 'codename': 'delete_product', 'module': 'inventory', 'description': 'Can delete products'},
|
||||
|
||||
# Purchasing permissions
|
||||
{'name': 'View Purchase Order', 'codename': 'view_purchaseorder', 'module': 'purchasing', 'description': 'Can view purchase orders'},
|
||||
{'name': 'Add Purchase Order', 'codename': 'add_purchaseorder', 'module': 'purchasing', 'description': 'Can create purchase orders'},
|
||||
{'name': 'Change Purchase Order', 'codename': 'change_purchaseorder', 'module': 'purchasing', 'description': 'Can edit purchase orders'},
|
||||
{'name': 'Delete Purchase Order', 'codename': 'delete_purchaseorder', 'module': 'purchasing', 'description': 'Can delete purchase orders'},
|
||||
|
||||
# Sales permissions
|
||||
{'name': 'View Sales Order', 'codename': 'view_salesorder', 'module': 'sales', 'description': 'Can view sales orders'},
|
||||
{'name': 'Add Sales Order', 'codename': 'add_salesorder', 'module': 'sales', 'description': 'Can create sales orders'},
|
||||
{'name': 'Change Sales Order', 'codename': 'change_salesorder', 'module': 'sales', 'description': 'Can edit sales orders'},
|
||||
{'name': 'Delete Sales Order', 'codename': 'delete_salesorder', 'module': 'sales', 'description': 'Can delete sales orders'},
|
||||
|
||||
# Manufacturing permissions
|
||||
{'name': 'View Manufacturing Order', 'codename': 'view_manufacturingorder', 'module': 'manufacturing', 'description': 'Can view manufacturing orders'},
|
||||
{'name': 'Add Manufacturing Order', 'codename': 'add_manufacturingorder', 'module': 'manufacturing', 'description': 'Can create manufacturing orders'},
|
||||
{'name': 'Change Manufacturing Order', 'codename': 'change_manufacturingorder', 'module': 'manufacturing', 'description': 'Can edit manufacturing orders'},
|
||||
{'name': 'Delete Manufacturing Order', 'codename': 'delete_manufacturingorder', 'module': 'manufacturing', 'description': 'Can delete manufacturing orders'},
|
||||
|
||||
# Reports permissions
|
||||
{'name': 'View Report', 'codename': 'view_report', 'module': 'reports', 'description': 'Can view reports'},
|
||||
{'name': 'Add Report', 'codename': 'add_report', 'module': 'reports', 'description': 'Can create reports'},
|
||||
{'name': 'Change Report', 'codename': 'change_report', 'module': 'reports', 'description': 'Can edit reports'},
|
||||
{'name': 'Delete Report', 'codename': 'delete_report', 'module': 'reports', 'description': 'Can delete reports'},
|
||||
|
||||
# Database management permissions
|
||||
{'name': 'View Database', 'codename': 'view_database', 'module': 'database_management', 'description': 'Can view database management'},
|
||||
{'name': 'Add Database', 'codename': 'add_database', 'module': 'database_management', 'description': 'Can create database entries'},
|
||||
{'name': 'Change Database', 'codename': 'change_database', 'module': 'database_management', 'description': 'Can edit database entries'},
|
||||
{'name': 'Delete Database', 'codename': 'delete_database', 'module': 'database_management', 'description': 'Can delete database entries'},
|
||||
]
|
||||
|
||||
# Create permissions if they don't exist
|
||||
for perm_data in default_permissions:
|
||||
Permission.objects.get_or_create(
|
||||
codename=perm_data['codename'],
|
||||
defaults=perm_data
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('role', 'permission')
|
||||
3
apps/accounts/tests.py
Normal file
3
apps/accounts/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
30
apps/accounts/urls.py
Normal file
30
apps/accounts/urls.py
Normal file
@ -0,0 +1,30 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'accounts'
|
||||
urlpatterns = [
|
||||
# Authentication
|
||||
path('login/', views.login_view, name='login'),
|
||||
path('logout/', views.logout_view, name='logout'),
|
||||
path('register/', views.register_view, name='register'),
|
||||
path('profile/', views.profile_view, name='profile'),
|
||||
path('profile/edit/', views.edit_profile_view, name='edit_profile'),
|
||||
|
||||
# User Management
|
||||
path('users/', views.user_list_view, name='user_list'),
|
||||
path('users/create/', views.create_user_view, name='create_user'),
|
||||
path('users/<int:user_id>/', views.user_detail_view, name='user_detail'),
|
||||
path('users/<int:user_id>/edit/', views.edit_user_view, name='edit_user'),
|
||||
path('users/<int:user_id>/delete/', views.delete_user_view, name='delete_user'),
|
||||
|
||||
# Role Management
|
||||
path('roles/', views.role_list_view, name='role_list'),
|
||||
path('roles/create/', views.create_role_view, name='create_role'),
|
||||
path('roles/<int:role_id>/', views.role_detail_view, name='role_detail'),
|
||||
path('roles/<int:role_id>/edit/', views.edit_role_view, name='edit_role'),
|
||||
path('roles/<int:role_id>/delete/', views.delete_role_view, name='delete_role'),
|
||||
path('roles/<int:role_id>/permissions/', views.assign_permissions_view, name='assign_permissions'),
|
||||
|
||||
# Permission Management
|
||||
path('permissions/', views.permission_list_view, name='permission_list'),
|
||||
]
|
||||
411
apps/accounts/views.py
Normal file
411
apps/accounts/views.py
Normal file
@ -0,0 +1,411 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from .models import User, Role, Permission, RolePermission, UserRole
|
||||
|
||||
|
||||
def login_view(request):
|
||||
"""User login view"""
|
||||
if request.method == 'POST':
|
||||
username = request.POST['username']
|
||||
password = request.POST['password']
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
return redirect('inventory:dashboard')
|
||||
else:
|
||||
messages.error(request, 'Invalid username or password')
|
||||
|
||||
context = {
|
||||
'module_title': 'Login',
|
||||
}
|
||||
return render(request, 'accounts/login.html', context)
|
||||
|
||||
|
||||
def logout_view(request):
|
||||
"""User logout view"""
|
||||
logout(request)
|
||||
messages.info(request, 'You have been logged out')
|
||||
return redirect('accounts:login')
|
||||
|
||||
|
||||
def register_view(request):
|
||||
"""User registration view"""
|
||||
# In a real app, this would contain registration logic
|
||||
return HttpResponse("Registration page")
|
||||
|
||||
|
||||
@login_required
|
||||
def profile_view(request):
|
||||
"""User profile view"""
|
||||
context = {
|
||||
'module_title': 'User Profile',
|
||||
}
|
||||
return render(request, 'accounts/profile.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_profile_view(request):
|
||||
"""Edit user profile view"""
|
||||
# In a real app, this would contain profile editing logic
|
||||
return HttpResponse("Edit profile page")
|
||||
|
||||
|
||||
# User Management Views
|
||||
@login_required
|
||||
@permission_required('accounts.view_user', raise_exception=True)
|
||||
def user_list_view(request):
|
||||
"""List all users"""
|
||||
users = User.objects.all()
|
||||
context = {
|
||||
'module_title': 'User Management',
|
||||
'users': users,
|
||||
}
|
||||
return render(request, 'accounts/user_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.add_user', raise_exception=True)
|
||||
def create_user_view(request):
|
||||
"""Create a new user"""
|
||||
if request.method == 'POST':
|
||||
from django.contrib import messages
|
||||
|
||||
username = request.POST.get('username')
|
||||
first_name = request.POST.get('first_name')
|
||||
last_name = request.POST.get('last_name')
|
||||
email = request.POST.get('email')
|
||||
password = request.POST.get('password')
|
||||
phone = request.POST.get('phone')
|
||||
department = request.POST.get('department')
|
||||
position = request.POST.get('position')
|
||||
is_active = request.POST.get('is_active') == 'on'
|
||||
roles = request.POST.getlist('roles')
|
||||
|
||||
try:
|
||||
# Create the user
|
||||
user = User.objects.create_user(
|
||||
username=username,
|
||||
email=email,
|
||||
password=password,
|
||||
first_name=first_name,
|
||||
last_name=last_name
|
||||
)
|
||||
user.phone = phone
|
||||
user.department = department
|
||||
user.position = position
|
||||
user.is_active = is_active
|
||||
user.save()
|
||||
|
||||
# Assign roles
|
||||
for role_id in roles:
|
||||
try:
|
||||
role = Role.objects.get(id=role_id)
|
||||
UserRole.objects.get_or_create(user=user, role=role)
|
||||
except Role.DoesNotExist:
|
||||
pass
|
||||
|
||||
messages.success(request, f'User {username} created successfully!')
|
||||
return redirect('accounts:user_detail', user_id=user.id)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error creating user: {str(e)}')
|
||||
|
||||
# GET request - show form
|
||||
roles = Role.objects.all()
|
||||
context = {
|
||||
'module_title': 'Create User',
|
||||
'roles': roles,
|
||||
}
|
||||
return render(request, 'accounts/create_user.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.view_user', raise_exception=True)
|
||||
def user_detail_view(request, user_id):
|
||||
"""View user details"""
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
context = {
|
||||
'module_title': 'User Details',
|
||||
'user': user,
|
||||
}
|
||||
return render(request, 'accounts/user_detail.html', context)
|
||||
except User.DoesNotExist:
|
||||
messages.error(request, 'User not found')
|
||||
return redirect('accounts:user_list')
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.change_user', raise_exception=True)
|
||||
def edit_user_view(request, user_id):
|
||||
"""Edit user details"""
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
messages.error(request, 'User not found')
|
||||
return redirect('accounts:user_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
from django.contrib import messages
|
||||
|
||||
# Update basic information
|
||||
user.first_name = request.POST.get('first_name')
|
||||
user.last_name = request.POST.get('last_name')
|
||||
user.email = request.POST.get('email')
|
||||
user.phone = request.POST.get('phone')
|
||||
user.department = request.POST.get('department')
|
||||
user.position = request.POST.get('position')
|
||||
user.is_active = request.POST.get('is_active') == 'on'
|
||||
|
||||
# Update password if provided
|
||||
password = request.POST.get('password')
|
||||
if password:
|
||||
user.set_password(password)
|
||||
|
||||
user.save()
|
||||
|
||||
# Update roles
|
||||
user.userrole_set.all().delete() # Remove existing roles
|
||||
roles = request.POST.getlist('roles')
|
||||
for role_id in roles:
|
||||
try:
|
||||
role = Role.objects.get(id=role_id)
|
||||
UserRole.objects.get_or_create(user=user, role=role)
|
||||
except Role.DoesNotExist:
|
||||
pass
|
||||
|
||||
messages.success(request, f'User {user.username} updated successfully!')
|
||||
return redirect('accounts:user_detail', user_id=user.id)
|
||||
|
||||
# GET request - show form
|
||||
roles = Role.objects.all()
|
||||
user_roles = user.userrole_set.values_list('role_id', flat=True)
|
||||
|
||||
context = {
|
||||
'module_title': 'Edit User',
|
||||
'user': user,
|
||||
'roles': roles,
|
||||
'user_roles': user_roles,
|
||||
}
|
||||
return render(request, 'accounts/edit_user.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.delete_user', raise_exception=True)
|
||||
def delete_user_view(request, user_id):
|
||||
"""Delete a user"""
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
messages.error(request, 'User not found')
|
||||
return redirect('accounts:user_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
username = user.username
|
||||
user.delete()
|
||||
messages.success(request, f'User {username} deleted successfully!')
|
||||
return redirect('accounts:user_list')
|
||||
|
||||
context = {
|
||||
'module_title': 'Delete User',
|
||||
'user': user,
|
||||
}
|
||||
return render(request, 'accounts/delete_user.html', context)
|
||||
|
||||
|
||||
# Role Management Views
|
||||
@login_required
|
||||
@permission_required('accounts.view_role', raise_exception=True)
|
||||
def role_list_view(request):
|
||||
"""List all roles"""
|
||||
roles = Role.objects.all()
|
||||
context = {
|
||||
'module_title': 'Role Management',
|
||||
'roles': roles,
|
||||
}
|
||||
return render(request, 'accounts/role_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.add_role', raise_exception=True)
|
||||
def create_role_view(request):
|
||||
"""Create a new role"""
|
||||
if request.method == 'POST':
|
||||
name = request.POST.get('name')
|
||||
description = request.POST.get('description')
|
||||
permissions = request.POST.getlist('permissions')
|
||||
|
||||
try:
|
||||
role = Role.objects.create(
|
||||
name=name,
|
||||
description=description
|
||||
)
|
||||
|
||||
# Assign permissions
|
||||
for perm_id in permissions:
|
||||
try:
|
||||
permission = Permission.objects.get(id=perm_id)
|
||||
RolePermission.objects.get_or_create(role=role, permission=permission)
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
|
||||
messages.success(request, f'Role {name} created successfully!')
|
||||
return redirect('accounts:role_detail', role_id=role.id)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error creating role: {str(e)}')
|
||||
|
||||
# GET request - show form
|
||||
permissions = Permission.objects.all()
|
||||
context = {
|
||||
'module_title': 'Create Role',
|
||||
'permissions': permissions,
|
||||
}
|
||||
return render(request, 'accounts/create_role.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.view_role', raise_exception=True)
|
||||
def role_detail_view(request, role_id):
|
||||
"""View role details"""
|
||||
try:
|
||||
role = Role.objects.get(id=role_id)
|
||||
context = {
|
||||
'module_title': 'Role Details',
|
||||
'role': role,
|
||||
}
|
||||
return render(request, 'accounts/role_detail.html', context)
|
||||
except Role.DoesNotExist:
|
||||
messages.error(request, 'Role not found')
|
||||
return redirect('accounts:role_list')
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.change_role', raise_exception=True)
|
||||
def edit_role_view(request, role_id):
|
||||
"""Edit role details"""
|
||||
try:
|
||||
role = Role.objects.get(id=role_id)
|
||||
except Role.DoesNotExist:
|
||||
messages.error(request, 'Role not found')
|
||||
return redirect('accounts:role_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
name = request.POST.get('name')
|
||||
description = request.POST.get('description')
|
||||
permissions = request.POST.getlist('permissions')
|
||||
|
||||
try:
|
||||
role.name = name
|
||||
role.description = description
|
||||
role.save()
|
||||
|
||||
# Update permissions
|
||||
role.rolepermission_set.all().delete() # Remove existing permissions
|
||||
for perm_id in permissions:
|
||||
try:
|
||||
permission = Permission.objects.get(id=perm_id)
|
||||
RolePermission.objects.get_or_create(role=role, permission=permission)
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
|
||||
messages.success(request, f'Role {name} updated successfully!')
|
||||
return redirect('accounts:role_detail', role_id=role.id)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error updating role: {str(e)}')
|
||||
|
||||
# GET request - show form
|
||||
permissions = Permission.objects.all()
|
||||
role_permissions = role.rolepermission_set.values_list('permission_id', flat=True)
|
||||
|
||||
context = {
|
||||
'module_title': 'Edit Role',
|
||||
'role': role,
|
||||
'permissions': permissions,
|
||||
'role_permissions': role_permissions,
|
||||
}
|
||||
return render(request, 'accounts/edit_role.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.delete_role', raise_exception=True)
|
||||
def delete_role_view(request, role_id):
|
||||
"""Delete a role"""
|
||||
try:
|
||||
role = Role.objects.get(id=role_id)
|
||||
except Role.DoesNotExist:
|
||||
messages.error(request, 'Role not found')
|
||||
return redirect('accounts:role_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
role_name = role.name
|
||||
role.delete()
|
||||
messages.success(request, f'Role {role_name} deleted successfully!')
|
||||
return redirect('accounts:role_list')
|
||||
|
||||
context = {
|
||||
'module_title': 'Delete Role',
|
||||
'role': role,
|
||||
}
|
||||
return render(request, 'accounts/delete_role.html', context)
|
||||
|
||||
|
||||
# Permission Management Views
|
||||
@login_required
|
||||
@permission_required('accounts.view_permission', raise_exception=True)
|
||||
def permission_list_view(request):
|
||||
"""List all permissions"""
|
||||
permissions = Permission.objects.all()
|
||||
context = {
|
||||
'module_title': 'Permission Management',
|
||||
'permissions': permissions,
|
||||
}
|
||||
return render(request, 'accounts/permission_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('accounts.change_role', raise_exception=True)
|
||||
def assign_permissions_view(request, role_id):
|
||||
"""Assign permissions to a role"""
|
||||
try:
|
||||
role = Role.objects.get(id=role_id)
|
||||
except Role.DoesNotExist:
|
||||
messages.error(request, 'Role not found')
|
||||
return redirect('accounts:role_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
permissions = request.POST.getlist('permissions')
|
||||
|
||||
try:
|
||||
# Remove existing permissions
|
||||
role.rolepermission_set.all().delete()
|
||||
|
||||
# Add new permissions
|
||||
for perm_id in permissions:
|
||||
try:
|
||||
permission = Permission.objects.get(id=perm_id)
|
||||
RolePermission.objects.get_or_create(role=role, permission=permission)
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
|
||||
messages.success(request, f'Permissions updated for role {role.name}!')
|
||||
return redirect('accounts:role_detail', role_id=role.id)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error updating permissions: {str(e)}')
|
||||
|
||||
# GET request - show form
|
||||
permissions = Permission.objects.all()
|
||||
role_permissions = role.rolepermission_set.values_list('permission_id', flat=True)
|
||||
|
||||
context = {
|
||||
'module_title': f'Assign Permissions to {role.name}',
|
||||
'role': role,
|
||||
'permissions': permissions,
|
||||
'role_permissions': role_permissions,
|
||||
}
|
||||
return render(request, 'accounts/assign_permissions.html', context)
|
||||
0
apps/database_management/__init__.py
Normal file
0
apps/database_management/__init__.py
Normal file
BIN
apps/database_management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/database_management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/database_management/__pycache__/admin.cpython-311.pyc
Normal file
BIN
apps/database_management/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/database_management/__pycache__/apps.cpython-311.pyc
Normal file
BIN
apps/database_management/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/database_management/__pycache__/models.cpython-311.pyc
Normal file
BIN
apps/database_management/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/database_management/__pycache__/urls.cpython-311.pyc
Normal file
BIN
apps/database_management/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/database_management/__pycache__/views.cpython-311.pyc
Normal file
BIN
apps/database_management/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
3
apps/database_management/admin.py
Normal file
3
apps/database_management/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
apps/database_management/apps.py
Normal file
6
apps/database_management/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DatabaseManagementConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.database_management"
|
||||
56
apps/database_management/migrations/0001_initial.py
Normal file
56
apps/database_management/migrations/0001_initial.py
Normal file
@ -0,0 +1,56 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 02:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DatabaseBackup",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("filename", models.CharField(max_length=25)),
|
||||
("file_path", models.CharField(max_length=500)),
|
||||
("size", models.BigIntegerField()),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("pending", "Pending"),
|
||||
("in_progress", "In Progress"),
|
||||
("completed", "Completed"),
|
||||
("failed", "Failed"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("completed_at", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
apps/database_management/migrations/__init__.py
Normal file
0
apps/database_management/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
21
apps/database_management/models.py
Normal file
21
apps/database_management/models.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class DatabaseBackup(models.Model):
|
||||
BACKUP_STATUS = [
|
||||
('pending', 'Pending'),
|
||||
('in_progress', 'In Progress'),
|
||||
('completed', 'Completed'),
|
||||
('failed', 'Failed'),
|
||||
]
|
||||
|
||||
filename = models.CharField(max_length=25)
|
||||
file_path = models.CharField(max_length=500)
|
||||
size = models.BigIntegerField()
|
||||
status = models.CharField(max_length=20, choices=BACKUP_STATUS)
|
||||
created_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.filename
|
||||
3
apps/database_management/tests.py
Normal file
3
apps/database_management/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
13
apps/database_management/urls.py
Normal file
13
apps/database_management/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'database_management'
|
||||
urlpatterns = [
|
||||
path('', views.db_dashboard, name='dashboard'),
|
||||
path('backup/', views.backup_view, name='backup'),
|
||||
path('restore/', views.restore_view, name='restore'),
|
||||
path('initialize/', views.initialize_view, name='initialize'),
|
||||
path('backups/', views.backup_list_view, name='backup_list'),
|
||||
path('backups/<int:backup_id>/download/', views.download_backup_view, name='download_backup'),
|
||||
path('backups/<int:backup_id>/delete/', views.delete_backup_view, name='delete_backup'),
|
||||
]
|
||||
267
apps/database_management/views.py
Normal file
267
apps/database_management/views.py
Normal file
@ -0,0 +1,267 @@
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.http import HttpResponse, Http404, JsonResponse
|
||||
from django.contrib import messages
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
from .models import DatabaseBackup
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('database_management.view_database', raise_exception=True)
|
||||
def db_dashboard(request):
|
||||
"""Database management dashboard view"""
|
||||
# Get database statistics
|
||||
try:
|
||||
import sqlite3
|
||||
db_path = settings.DATABASES['default']['NAME']
|
||||
if os.path.exists(db_path):
|
||||
db_size = os.path.getsize(db_path)
|
||||
db_size_mb = db_size / (1024 * 1024)
|
||||
|
||||
# Get table count
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = cursor.fetchall()
|
||||
table_count = len(tables)
|
||||
conn.close()
|
||||
else:
|
||||
db_size_mb = 0
|
||||
table_count = 0
|
||||
|
||||
# Get last backup info
|
||||
last_backup = DatabaseBackup.objects.filter(status='completed').order_by('-created_at').first()
|
||||
last_backup_info = last_backup.completed_at.strftime('%Y-%m-%d %I:%M %p') if last_backup else 'No backups yet'
|
||||
|
||||
except Exception as e:
|
||||
db_size_mb = 0
|
||||
table_count = 0
|
||||
last_backup_info = 'Unable to retrieve'
|
||||
|
||||
context = {
|
||||
'module_title': 'Database Management',
|
||||
'db_size': f"{db_size_mb:.1f} MB",
|
||||
'table_count': f"{table_count} tables",
|
||||
'last_backup': last_backup_info,
|
||||
}
|
||||
return render(request, 'database_management/dashboard.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('database_management.add_database', raise_exception=True)
|
||||
def backup_view(request):
|
||||
"""Create a database backup"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
# Generate backup filename
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"backup_{timestamp}.db"
|
||||
|
||||
# Database path
|
||||
db_path = settings.DATABASES['default']['NAME']
|
||||
|
||||
# Create backup directory if it doesn't exist
|
||||
backup_dir = os.path.join(settings.MEDIA_ROOT, 'database_backups')
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
backup_path = os.path.join(backup_dir, filename)
|
||||
|
||||
# Copy database file
|
||||
import shutil
|
||||
shutil.copy2(db_path, backup_path)
|
||||
|
||||
# Get file size
|
||||
file_size = os.path.getsize(backup_path)
|
||||
|
||||
# Save backup record
|
||||
backup = DatabaseBackup.objects.create(
|
||||
filename=filename,
|
||||
file_path=backup_path,
|
||||
size=file_size,
|
||||
status='completed',
|
||||
created_by=request.user,
|
||||
completed_at=datetime.now()
|
||||
)
|
||||
|
||||
messages.success(request, f'Database backup created successfully: {filename}')
|
||||
return redirect('database_management:backup_list')
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error creating backup: {str(e)}')
|
||||
return redirect('database_management:db_dashboard')
|
||||
|
||||
context = {
|
||||
'module_title': 'Create Database Backup',
|
||||
}
|
||||
return render(request, 'database_management/backup.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('database_management.change_database', raise_exception=True)
|
||||
def restore_view(request):
|
||||
"""Restore a database backup"""
|
||||
if request.method == 'POST' and request.FILES.get('backup_file'):
|
||||
try:
|
||||
backup_file = request.FILES['backup_file']
|
||||
|
||||
# Validate file extension
|
||||
if not backup_file.name.endswith(('.db', '.sqlite3')):
|
||||
messages.error(request, 'Invalid file type. Please upload a SQLite database file.')
|
||||
return redirect('database_management:db_dashboard')
|
||||
|
||||
# Database path
|
||||
db_path = settings.DATABASES['default']['NAME']
|
||||
|
||||
# Create backup of current database before restore
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_filename = f"pre_restore_backup_{timestamp}.db"
|
||||
backup_dir = os.path.join(settings.MEDIA_ROOT, 'database_backups')
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
pre_restore_path = os.path.join(backup_dir, backup_filename)
|
||||
|
||||
import shutil
|
||||
if os.path.exists(db_path):
|
||||
shutil.copy2(db_path, pre_restore_path)
|
||||
|
||||
# Save pre-restore backup record
|
||||
pre_backup_size = os.path.getsize(pre_restore_path)
|
||||
DatabaseBackup.objects.create(
|
||||
filename=backup_filename,
|
||||
file_path=pre_restore_path,
|
||||
size=pre_backup_size,
|
||||
status='completed',
|
||||
created_by=request.user,
|
||||
completed_at=datetime.now()
|
||||
)
|
||||
|
||||
# Save uploaded file temporarily
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_path = os.path.join(temp_dir, 'uploaded_backup.db')
|
||||
|
||||
with open(temp_path, 'wb+') as destination:
|
||||
for chunk in backup_file.chunks():
|
||||
destination.write(chunk)
|
||||
|
||||
# Validate uploaded file is a valid SQLite database
|
||||
import sqlite3
|
||||
try:
|
||||
conn = sqlite3.connect(temp_path)
|
||||
conn.close()
|
||||
except sqlite3.Error:
|
||||
messages.error(request, 'Invalid database file. Please upload a valid SQLite database.')
|
||||
os.remove(temp_path)
|
||||
os.rmdir(temp_dir)
|
||||
return redirect('database_management:db_dashboard')
|
||||
|
||||
# Replace current database with uploaded backup
|
||||
shutil.move(temp_path, db_path)
|
||||
os.rmdir(temp_dir)
|
||||
|
||||
messages.success(request, 'Database restored successfully from backup file.')
|
||||
messages.info(request, f'A pre-restore backup was created: {backup_filename}')
|
||||
|
||||
return redirect('database_management:db_dashboard')
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error restoring database: {str(e)}')
|
||||
return redirect('database_management:db_dashboard')
|
||||
|
||||
context = {
|
||||
'module_title': 'Restore Database',
|
||||
}
|
||||
return render(request, 'database_management/restore.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('database_management.add_database', raise_exception=True)
|
||||
def initialize_view(request):
|
||||
"""Initialize the database"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
# This would typically run migrations or initialization scripts
|
||||
# For now, just show a message
|
||||
messages.success(request, 'Database initialization completed successfully.')
|
||||
return redirect('database_management:db_dashboard')
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error initializing database: {str(e)}')
|
||||
return redirect('database_management:db_dashboard')
|
||||
|
||||
context = {
|
||||
'module_title': 'Initialize Database',
|
||||
}
|
||||
return render(request, 'database_management/initialize.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('database_management.view_database', raise_exception=True)
|
||||
def backup_list_view(request):
|
||||
"""List all database backups"""
|
||||
backups = DatabaseBackup.objects.all().order_by('-created_at')
|
||||
context = {
|
||||
'module_title': 'Database Backup List',
|
||||
'backups': backups,
|
||||
}
|
||||
return render(request, 'database_management/backup_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('database_management.view_database', raise_exception=True)
|
||||
def download_backup_view(request, backup_id):
|
||||
"""Download a database backup"""
|
||||
try:
|
||||
backup = get_object_or_404(DatabaseBackup, id=backup_id)
|
||||
|
||||
if not os.path.exists(backup.file_path):
|
||||
messages.error(request, 'Backup file not found on disk.')
|
||||
return redirect('database_management:backup_list')
|
||||
|
||||
# Read file content
|
||||
with open(backup.file_path, 'rb') as f:
|
||||
file_data = f.read()
|
||||
|
||||
# Return file as response
|
||||
response = HttpResponse(file_data, content_type='application/octet-stream')
|
||||
response['Content-Disposition'] = f'attachment; filename="{backup.filename}"'
|
||||
response['Content-Length'] = len(file_data)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error downloading backup: {str(e)}')
|
||||
return redirect('database_management:backup_list')
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('database_management.delete_database', raise_exception=True)
|
||||
def delete_backup_view(request, backup_id):
|
||||
"""Delete a database backup"""
|
||||
try:
|
||||
backup = get_object_or_404(DatabaseBackup, id=backup_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
# Delete file from disk if it exists
|
||||
if os.path.exists(backup.file_path):
|
||||
os.remove(backup.file_path)
|
||||
|
||||
# Delete database record
|
||||
backup.delete()
|
||||
|
||||
messages.success(request, f'Backup {backup.filename} deleted successfully.')
|
||||
return redirect('database_management:backup_list')
|
||||
|
||||
context = {
|
||||
'module_title': 'Delete Backup',
|
||||
'backup': backup,
|
||||
}
|
||||
return render(request, 'database_management/delete_backup.html', context)
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error deleting backup: {str(e)}')
|
||||
return redirect('database_management:backup_list')
|
||||
0
apps/inventory/__init__.py
Normal file
0
apps/inventory/__init__.py
Normal file
BIN
apps/inventory/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/inventory/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/inventory/__pycache__/admin.cpython-311.pyc
Normal file
BIN
apps/inventory/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/inventory/__pycache__/apps.cpython-311.pyc
Normal file
BIN
apps/inventory/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/inventory/__pycache__/models.cpython-311.pyc
Normal file
BIN
apps/inventory/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/inventory/__pycache__/urls.cpython-311.pyc
Normal file
BIN
apps/inventory/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/inventory/__pycache__/views.cpython-311.pyc
Normal file
BIN
apps/inventory/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
3
apps/inventory/admin.py
Normal file
3
apps/inventory/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
apps/inventory/apps.py
Normal file
6
apps/inventory/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InventoryConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.inventory"
|
||||
225
apps/inventory/migrations/0001_initial.py
Normal file
225
apps/inventory/migrations/0001_initial.py
Normal file
@ -0,0 +1,225 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 02:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
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)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Product",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("code", models.CharField(max_length=50, unique=True)),
|
||||
("name", models.CharField(max_length=200)),
|
||||
("description", models.TextField(blank=True)),
|
||||
(
|
||||
"product_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("raw_material", "Raw Material"),
|
||||
("finished_good", "Finished Good"),
|
||||
("component", "Component"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"reorder_level",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||
),
|
||||
(
|
||||
"selling_price",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=10, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"cost_price",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=10, null=True
|
||||
),
|
||||
),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"category",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="inventory.category",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UnitOfMeasure",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=50)),
|
||||
("abbreviation", models.CharField(max_length=10)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Warehouse",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
("location", models.CharField(max_length=200)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="StockMovement",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"movement_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("in", "Stock In"),
|
||||
("out", "Stock Out"),
|
||||
("adjustment", "Adjustment"),
|
||||
("transfer", "Transfer"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("quantity", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
("reference_number", models.CharField(blank=True, max_length=100)),
|
||||
("notes", models.TextField(blank=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="inventory.product",
|
||||
),
|
||||
),
|
||||
(
|
||||
"warehouse",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="inventory.warehouse",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="unit_of_measure",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="inventory.unitofmeasure",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Inventory",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"quantity",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||
),
|
||||
(
|
||||
"reserved_quantity",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||
),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="inventory.product",
|
||||
),
|
||||
),
|
||||
(
|
||||
"warehouse",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="inventory.warehouse",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("product", "warehouse")},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
apps/inventory/migrations/__init__.py
Normal file
0
apps/inventory/migrations/__init__.py
Normal file
Binary file not shown.
BIN
apps/inventory/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/inventory/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
86
apps/inventory/models.py
Normal file
86
apps/inventory/models.py
Normal file
@ -0,0 +1,86 @@
|
||||
from django.db import models
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class UnitOfMeasure(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
abbreviation = models.CharField(max_length=10)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
PRODUCT_TYPES = [
|
||||
('raw_material', 'Raw Material'),
|
||||
('finished_good', 'Finished Good'),
|
||||
('component', 'Component'),
|
||||
]
|
||||
|
||||
code = models.CharField(max_length=50, unique=True)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True)
|
||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
unit_of_measure = models.ForeignKey(UnitOfMeasure, on_delete=models.SET_NULL, null=True)
|
||||
product_type = models.CharField(max_length=20, choices=PRODUCT_TYPES)
|
||||
reorder_level = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||
selling_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
cost_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} - {self.name}"
|
||||
|
||||
|
||||
class Warehouse(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
location = models.CharField(max_length=200)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Inventory(models.Model):
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE)
|
||||
quantity = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||
reserved_quantity = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('product', 'warehouse')
|
||||
|
||||
@property
|
||||
def available_quantity(self):
|
||||
return self.quantity - self.reserved_quantity
|
||||
|
||||
|
||||
class StockMovement(models.Model):
|
||||
MOVEMENT_TYPES = [
|
||||
('in', 'Stock In'),
|
||||
('out', 'Stock Out'),
|
||||
('adjustment', 'Adjustment'),
|
||||
('transfer', 'Transfer'),
|
||||
]
|
||||
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE)
|
||||
movement_type = models.CharField(max_length=20, choices=MOVEMENT_TYPES)
|
||||
quantity = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
reference_number = models.CharField(max_length=100, blank=True)
|
||||
notes = models.TextField(blank=True)
|
||||
created_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
0
apps/inventory/templatetags/__init__.py
Normal file
0
apps/inventory/templatetags/__init__.py
Normal file
BIN
apps/inventory/templatetags/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/inventory/templatetags/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
36
apps/inventory/templatetags/indonesian_filters.py
Normal file
36
apps/inventory/templatetags/indonesian_filters.py
Normal file
@ -0,0 +1,36 @@
|
||||
from django import template
|
||||
from decimal import Decimal
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def indonesian_number(value, decimal_places=2):
|
||||
"""
|
||||
Format numbers using Indonesian locale (dot as thousand separator, comma as decimal separator)
|
||||
"""
|
||||
if value is None or value == '':
|
||||
return '0,00'
|
||||
|
||||
try:
|
||||
# Convert to Decimal for precise handling
|
||||
if not isinstance(value, Decimal):
|
||||
value = Decimal(str(value))
|
||||
|
||||
# Format the number with thousand separators using English locale first
|
||||
formatted = f"{value:,.{decimal_places}f}"
|
||||
|
||||
# Convert to Indonesian format: comma to dot, dot to comma
|
||||
formatted = formatted.replace(',', 'temp_comma').replace('.', ',').replace('temp_comma', '.')
|
||||
|
||||
return formatted
|
||||
except (ValueError, TypeError):
|
||||
return '0,00'
|
||||
|
||||
@register.filter
|
||||
def format_rupiah(value, decimal_places=2):
|
||||
"""
|
||||
Format numbers as Indonesian Rupiah currency
|
||||
"""
|
||||
formatted_number = indonesian_number(value, decimal_places)
|
||||
return f"Rp {formatted_number}"
|
||||
3
apps/inventory/tests.py
Normal file
3
apps/inventory/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
44
apps/inventory/urls.py
Normal file
44
apps/inventory/urls.py
Normal file
@ -0,0 +1,44 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'inventory'
|
||||
urlpatterns = [
|
||||
# Dashboard
|
||||
path('', views.inventory_dashboard, name='dashboard'),
|
||||
|
||||
# Products
|
||||
path('products/', views.product_list_view, name='product_list'),
|
||||
path('products/create/', views.create_product_view, name='create_product'),
|
||||
path('products/<int:product_id>/', views.product_detail_view, name='product_detail'),
|
||||
path('products/<int:product_id>/edit/', views.edit_product_view, name='edit_product'),
|
||||
path('products/<int:product_id>/delete/', views.delete_product_view, name='delete_product'),
|
||||
|
||||
# Categories
|
||||
path('categories/', views.category_list_view, name='category_list'),
|
||||
path('categories/create/', views.create_category_view, name='create_category'),
|
||||
path('categories/<int:category_id>/edit/', views.edit_category_view, name='edit_category'),
|
||||
path('categories/<int:category_id>/delete/', views.delete_category_view, name='delete_category'),
|
||||
|
||||
# Units of Measure
|
||||
path('uom/', views.uom_list_view, name='uom_list'),
|
||||
path('uom/create/', views.create_uom_view, name='create_uom'),
|
||||
path('uom/<int:uom_id>/edit/', views.edit_uom_view, name='edit_uom'),
|
||||
path('uom/<int:uom_id>/delete/', views.delete_uom_view, name='delete_uom'),
|
||||
|
||||
# Warehouses
|
||||
path('warehouses/', views.warehouse_list_view, name='warehouse_list'),
|
||||
path('warehouses/create/', views.create_warehouse_view, name='create_warehouse'),
|
||||
path('warehouses/<int:warehouse_id>/', views.warehouse_detail_view, name='warehouse_detail'),
|
||||
path('warehouses/<int:warehouse_id>/edit/', views.edit_warehouse_view, name='edit_warehouse'),
|
||||
path('warehouses/<int:warehouse_id>/delete/', views.delete_warehouse_view, name='delete_warehouse'),
|
||||
|
||||
# Stock Movements
|
||||
path('movements/', views.stock_movement_list_view, name='movement_list'),
|
||||
path('movements/in/', views.stock_in_view, name='stock_in'),
|
||||
path('movements/out/', views.stock_out_view, name='stock_out'),
|
||||
path('movements/adjustment/', views.stock_adjustment_view, name='stock_adjustment'),
|
||||
path('movements/transfer/', views.stock_transfer_view, name='stock_transfer'),
|
||||
|
||||
# API endpoints
|
||||
path('api/products/', views.product_api_view, name='product_api'),
|
||||
]
|
||||
694
apps/inventory/views.py
Normal file
694
apps/inventory/views.py
Normal file
@ -0,0 +1,694 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.db.models import Sum, Q, Count
|
||||
from django import forms
|
||||
from django.utils import timezone
|
||||
from .models import Product, Category, UnitOfMeasure, Warehouse, StockMovement, Inventory
|
||||
import locale
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
|
||||
class IndonesianNumberInput(forms.NumberInput):
|
||||
"""Custom widget for Indonesian number input formatting"""
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
# Set step to 0.01 for decimal fields
|
||||
if 'step' not in attrs:
|
||||
attrs['step'] = '0.01'
|
||||
# Add placeholder for better UX
|
||||
if 'placeholder' not in attrs:
|
||||
attrs['placeholder'] = '0.00'
|
||||
super().__init__(attrs)
|
||||
|
||||
def format_value(self, value):
|
||||
"""Format the value for display in the input field"""
|
||||
if value is None or value == '':
|
||||
return None
|
||||
|
||||
try:
|
||||
# Convert to Decimal for precise handling
|
||||
if not isinstance(value, Decimal):
|
||||
value = Decimal(str(value))
|
||||
|
||||
# Format with Indonesian decimal separator (comma)
|
||||
formatted = '{:.2f}'.format(value)
|
||||
return formatted.replace('.', ',')
|
||||
except (ValueError, TypeError):
|
||||
return super().format_value(value)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""Handle value from form data"""
|
||||
value = super().value_from_datadict(data, files, name)
|
||||
if value is None or value == '':
|
||||
return value
|
||||
|
||||
try:
|
||||
# Clean up any formatting and convert to Decimal
|
||||
if isinstance(value, str):
|
||||
# Remove any thousand separators and convert comma to dot
|
||||
value = value.replace('.', '').replace(',', '.')
|
||||
return Decimal(value)
|
||||
except (ValueError, TypeError, InvalidOperation):
|
||||
return value
|
||||
|
||||
|
||||
class ProductForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['code', 'name', 'description', 'category', 'unit_of_measure',
|
||||
'product_type', 'reorder_level', 'selling_price', 'cost_price', 'is_active']
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
'reorder_level': IndonesianNumberInput(attrs={'min': 0}),
|
||||
'selling_price': IndonesianNumberInput(attrs={'min': 0, 'step': '0.01'}),
|
||||
'cost_price': IndonesianNumberInput(attrs={'min': 0, 'step': '0.01'}),
|
||||
}
|
||||
|
||||
|
||||
class CategoryForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ['name', 'description']
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
|
||||
class UnitOfMeasureForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UnitOfMeasure
|
||||
fields = ['name', 'abbreviation']
|
||||
|
||||
|
||||
class WarehouseForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Warehouse
|
||||
fields = ['name', 'location', 'is_active']
|
||||
|
||||
|
||||
@login_required
|
||||
def inventory_dashboard(request):
|
||||
"""Inventory dashboard view"""
|
||||
products = Product.objects.filter(is_active=True)
|
||||
warehouses = Warehouse.objects.filter(is_active=True)
|
||||
categories = Category.objects.all()
|
||||
|
||||
# Get low stock products (below reorder level)
|
||||
low_stock_products = []
|
||||
for product in products:
|
||||
total_quantity = Inventory.objects.filter(
|
||||
product=product
|
||||
).aggregate(total=Sum('quantity'))['total'] or 0
|
||||
|
||||
if total_quantity <= product.reorder_level:
|
||||
low_stock_products.append({
|
||||
'name': product.name,
|
||||
'total_quantity': total_quantity,
|
||||
'reorder_level': product.reorder_level,
|
||||
})
|
||||
|
||||
# Get recent stock movements
|
||||
recent_movements = StockMovement.objects.select_related(
|
||||
'product', 'warehouse'
|
||||
).order_by('-created_at')[:10]
|
||||
|
||||
context = {
|
||||
'module_title': 'Inventory Management',
|
||||
'products': products,
|
||||
'warehouses': warehouses,
|
||||
'categories': categories,
|
||||
'low_stock_products': low_stock_products,
|
||||
'recent_movements': recent_movements,
|
||||
}
|
||||
return render(request, 'inventory/dashboard.html', context)
|
||||
|
||||
|
||||
# Product Views
|
||||
@login_required
|
||||
@permission_required('inventory.view_product', raise_exception=True)
|
||||
def product_list_view(request):
|
||||
"""List all products"""
|
||||
products = Product.objects.all()
|
||||
|
||||
# Add current stock information to each product
|
||||
for product in products:
|
||||
total_stock = Inventory.objects.filter(product=product).aggregate(
|
||||
total=Sum('quantity')
|
||||
)['total'] or 0
|
||||
product.current_stock = total_stock
|
||||
|
||||
context = {
|
||||
'module_title': 'Product List',
|
||||
'products': products,
|
||||
}
|
||||
return render(request, 'inventory/product_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.add_product', raise_exception=True)
|
||||
def create_product_view(request):
|
||||
"""Create a new product"""
|
||||
if request.method == 'POST':
|
||||
form = ProductForm(request.POST)
|
||||
if form.is_valid():
|
||||
product = form.save()
|
||||
messages.success(request, f'Product "{product.name}" created successfully!')
|
||||
return redirect('inventory:product_detail', product_id=product.id)
|
||||
else:
|
||||
form = ProductForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'module_title': 'Create Product',
|
||||
'is_create': True,
|
||||
}
|
||||
return render(request, 'inventory/product_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.view_product', raise_exception=True)
|
||||
def product_detail_view(request, product_id):
|
||||
"""View product details"""
|
||||
try:
|
||||
product = Product.objects.get(id=product_id)
|
||||
inventory_items = Inventory.objects.filter(product=product).select_related('warehouse')
|
||||
total_stock = inventory_items.aggregate(total=Sum('quantity'))['total'] or 0
|
||||
recent_movements = StockMovement.objects.filter(
|
||||
product=product
|
||||
).select_related('warehouse').order_by('-created_at')[:10]
|
||||
|
||||
context = {
|
||||
'module_title': 'Product Details',
|
||||
'product': product,
|
||||
'inventory_items': inventory_items,
|
||||
'total_stock': total_stock,
|
||||
'recent_movements': recent_movements,
|
||||
}
|
||||
return render(request, 'inventory/product_detail.html', context)
|
||||
except Product.DoesNotExist:
|
||||
messages.error(request, 'Product not found')
|
||||
return redirect('inventory:product_list')
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.change_product', raise_exception=True)
|
||||
def edit_product_view(request, product_id):
|
||||
"""Edit product details"""
|
||||
product = get_object_or_404(Product, id=product_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ProductForm(request.POST, instance=product)
|
||||
if form.is_valid():
|
||||
product = form.save()
|
||||
messages.success(request, f'Product "{product.name}" updated successfully!')
|
||||
return redirect('inventory:product_detail', product_id=product.id)
|
||||
else:
|
||||
form = ProductForm(instance=product)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'product': product,
|
||||
'module_title': 'Edit Product',
|
||||
'is_create': False,
|
||||
}
|
||||
return render(request, 'inventory/product_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_product_view(request, product_id):
|
||||
"""Delete a product"""
|
||||
product = get_object_or_404(Product, id=product_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
# Simple delete without permission checks for now
|
||||
try:
|
||||
product_name = product.name
|
||||
product.delete()
|
||||
messages.success(request, f'Product "{product_name}" deleted successfully!')
|
||||
return redirect('inventory:product_list')
|
||||
except Exception as e:
|
||||
messages.error(request, f'Error deleting product: {str(e)}')
|
||||
return redirect('inventory:product_detail', product_id=product.id)
|
||||
|
||||
# Get product statistics for confirmation
|
||||
inventory_count = Inventory.objects.filter(product=product).count()
|
||||
movement_count = StockMovement.objects.filter(product=product).count()
|
||||
total_stock = Inventory.objects.filter(product=product).aggregate(
|
||||
total=Sum('quantity')
|
||||
)['total'] or 0
|
||||
|
||||
context = {
|
||||
'product': product,
|
||||
'module_title': 'Delete Product',
|
||||
'inventory_count': inventory_count,
|
||||
'movement_count': movement_count,
|
||||
'total_stock': total_stock,
|
||||
}
|
||||
return render(request, 'inventory/product_confirm_delete.html', context)
|
||||
|
||||
|
||||
# Category Views
|
||||
@login_required
|
||||
@permission_required('inventory.view_category', raise_exception=True)
|
||||
def category_list_view(request):
|
||||
"""List all categories"""
|
||||
categories = Category.objects.all()
|
||||
context = {
|
||||
'module_title': 'Category List',
|
||||
'categories': categories,
|
||||
}
|
||||
return render(request, 'inventory/category_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.add_category', raise_exception=True)
|
||||
def create_category_view(request):
|
||||
"""Create a new category"""
|
||||
if request.method == 'POST':
|
||||
form = CategoryForm(request.POST)
|
||||
if form.is_valid():
|
||||
category = form.save()
|
||||
messages.success(request, f'Category "{category.name}" created successfully!')
|
||||
return redirect('inventory:category_list')
|
||||
else:
|
||||
form = CategoryForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'module_title': 'Create Category',
|
||||
}
|
||||
return render(request, 'inventory/category_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.change_category', raise_exception=True)
|
||||
def edit_category_view(request, category_id):
|
||||
"""Edit category details"""
|
||||
# In a real app, this would contain category editing logic
|
||||
return HttpResponse("Edit category page")
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.delete_category', raise_exception=True)
|
||||
def delete_category_view(request, category_id):
|
||||
"""Delete a category"""
|
||||
# In a real app, this would contain category deletion logic
|
||||
return HttpResponse("Delete category page")
|
||||
|
||||
|
||||
# Unit of Measure Views
|
||||
@login_required
|
||||
@permission_required('inventory.view_unitofmeasure', raise_exception=True)
|
||||
def uom_list_view(request):
|
||||
"""List all units of measure"""
|
||||
uoms = UnitOfMeasure.objects.all()
|
||||
context = {
|
||||
'module_title': 'Unit of Measure List',
|
||||
'uoms': uoms,
|
||||
}
|
||||
return render(request, 'inventory/uom_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.add_unitofmeasure', raise_exception=True)
|
||||
def create_uom_view(request):
|
||||
"""Create a new unit of measure"""
|
||||
# In a real app, this would contain UOM creation logic
|
||||
return HttpResponse("Create unit of measure page")
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.change_unitofmeasure', raise_exception=True)
|
||||
def edit_uom_view(request, uom_id):
|
||||
"""Edit unit of measure details"""
|
||||
# In a real app, this would contain UOM editing logic
|
||||
return HttpResponse("Edit unit of measure page")
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.delete_unitofmeasure', raise_exception=True)
|
||||
def delete_uom_view(request, uom_id):
|
||||
"""Delete a unit of measure"""
|
||||
# In a real app, this would contain UOM deletion logic
|
||||
return HttpResponse("Delete unit of measure page")
|
||||
|
||||
|
||||
# Warehouse Views
|
||||
@login_required
|
||||
@permission_required('inventory.view_warehouse', raise_exception=True)
|
||||
def warehouse_list_view(request):
|
||||
"""List all warehouses"""
|
||||
warehouses = Warehouse.objects.prefetch_related('inventory_set').all()
|
||||
|
||||
# Add inventory count to each warehouse
|
||||
for warehouse in warehouses:
|
||||
warehouse.inventory_count = warehouse.inventory_set.count()
|
||||
|
||||
context = {
|
||||
'module_title': 'Warehouse List',
|
||||
'warehouses': warehouses,
|
||||
}
|
||||
return render(request, 'inventory/warehouse_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.view_warehouse', raise_exception=True)
|
||||
def warehouse_detail_view(request, warehouse_id):
|
||||
"""View warehouse details"""
|
||||
try:
|
||||
warehouse = Warehouse.objects.get(id=warehouse_id)
|
||||
inventory_items = Inventory.objects.filter(warehouse=warehouse).select_related('product')
|
||||
total_products = inventory_items.count()
|
||||
total_value = sum(item.quantity * item.product.cost_price for item in inventory_items)
|
||||
|
||||
# Add total_value to each inventory item for template use
|
||||
for item in inventory_items:
|
||||
item.total_value = item.quantity * item.product.cost_price
|
||||
|
||||
context = {
|
||||
'module_title': 'Warehouse Details',
|
||||
'warehouse': warehouse,
|
||||
'inventory_items': inventory_items,
|
||||
'total_products': total_products,
|
||||
'total_value': total_value,
|
||||
}
|
||||
return render(request, 'inventory/warehouse_detail.html', context)
|
||||
except Warehouse.DoesNotExist:
|
||||
messages.error(request, 'Warehouse not found')
|
||||
return redirect('inventory:warehouse_list')
|
||||
|
||||
|
||||
@login_required
|
||||
def create_warehouse_view(request):
|
||||
"""Create a new warehouse"""
|
||||
if request.method == 'POST':
|
||||
form = WarehouseForm(request.POST)
|
||||
if form.is_valid():
|
||||
warehouse = form.save()
|
||||
messages.success(request, f'Warehouse "{warehouse.name}" created successfully!')
|
||||
return redirect('inventory:warehouse_list')
|
||||
else:
|
||||
form = WarehouseForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'module_title': 'Create Warehouse',
|
||||
'is_create': True,
|
||||
}
|
||||
return render(request, 'inventory/warehouse_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_warehouse_view(request, warehouse_id):
|
||||
"""Edit warehouse details"""
|
||||
warehouse = get_object_or_404(Warehouse, id=warehouse_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = WarehouseForm(request.POST, instance=warehouse)
|
||||
if form.is_valid():
|
||||
warehouse = form.save()
|
||||
messages.success(request, f'Warehouse "{warehouse.name}" updated successfully!')
|
||||
return redirect('inventory:warehouse_list')
|
||||
else:
|
||||
form = WarehouseForm(instance=warehouse)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'warehouse': warehouse,
|
||||
'module_title': 'Edit Warehouse',
|
||||
'is_create': False,
|
||||
}
|
||||
return render(request, 'inventory/warehouse_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_warehouse_view(request, warehouse_id):
|
||||
"""Delete a warehouse"""
|
||||
warehouse = get_object_or_404(Warehouse, id=warehouse_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
warehouse_name = warehouse.name
|
||||
warehouse.delete()
|
||||
messages.success(request, f'Warehouse "{warehouse_name}" deleted successfully!')
|
||||
return redirect('inventory:warehouse_list')
|
||||
|
||||
# Get warehouse statistics for confirmation
|
||||
inventory_items = Inventory.objects.filter(warehouse=warehouse).select_related('product')
|
||||
inventory_count = inventory_items.count()
|
||||
total_products = inventory_items.values('product').distinct().count()
|
||||
total_value = sum(item.quantity * item.product.cost_price for item in inventory_items)
|
||||
|
||||
context = {
|
||||
'warehouse': warehouse,
|
||||
'module_title': 'Delete Warehouse',
|
||||
'inventory_count': inventory_count,
|
||||
'total_products': total_products,
|
||||
'total_value': total_value,
|
||||
}
|
||||
return render(request, 'inventory/warehouse_confirm_delete.html', context)
|
||||
|
||||
|
||||
# Stock Movement Views
|
||||
@login_required
|
||||
@permission_required('inventory.view_stockmovement', raise_exception=True)
|
||||
def stock_movement_list_view(request):
|
||||
"""List all stock movements"""
|
||||
movements = StockMovement.objects.all()
|
||||
context = {
|
||||
'module_title': 'Stock Movement List',
|
||||
'movements': movements,
|
||||
}
|
||||
return render(request, 'inventory/movement_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.add_stockmovement', raise_exception=True)
|
||||
def stock_in_view(request):
|
||||
"""Add stock to inventory"""
|
||||
if request.method == 'POST':
|
||||
product_id = request.POST.get('product')
|
||||
warehouse_id = request.POST.get('warehouse')
|
||||
quantity_str = request.POST.get('quantity', '0').replace(',', '.')
|
||||
|
||||
try:
|
||||
product = Product.objects.get(id=product_id)
|
||||
warehouse = Warehouse.objects.get(id=warehouse_id)
|
||||
quantity = Decimal(quantity_str)
|
||||
|
||||
if quantity <= 0:
|
||||
messages.error(request, 'Quantity must be greater than 0')
|
||||
return redirect('inventory:stock_in')
|
||||
|
||||
# Create or update inventory
|
||||
inventory, created = Inventory.objects.get_or_create(
|
||||
product=product,
|
||||
warehouse=warehouse,
|
||||
defaults={'quantity': 0}
|
||||
)
|
||||
inventory.quantity += quantity
|
||||
inventory.save()
|
||||
|
||||
# Create stock movement record
|
||||
StockMovement.objects.create(
|
||||
product=product,
|
||||
warehouse=warehouse,
|
||||
movement_type='in',
|
||||
quantity=quantity,
|
||||
reference_number=f"STKIN-{timezone.now().strftime('%Y%m%d-%H%M%S')}",
|
||||
notes=request.POST.get('notes', ''),
|
||||
created_by=request.user
|
||||
)
|
||||
|
||||
messages.success(request, f'Successfully added {quantity} {product.unit_of_measure.abbreviation} of {product.name} to {warehouse.name}')
|
||||
return redirect('inventory:movement_list')
|
||||
|
||||
except (Product.DoesNotExist, Warehouse.DoesNotExist):
|
||||
messages.error(request, 'Invalid product or warehouse selected')
|
||||
except (ValueError, InvalidOperation):
|
||||
messages.error(request, 'Invalid quantity entered')
|
||||
|
||||
# GET request - show form
|
||||
context = {
|
||||
'module_title': 'Stock In',
|
||||
'products': Product.objects.filter(is_active=True),
|
||||
'warehouses': Warehouse.objects.filter(is_active=True),
|
||||
}
|
||||
return render(request, 'inventory/stock_in.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.add_stockmovement', raise_exception=True)
|
||||
def stock_out_view(request):
|
||||
"""Remove stock from inventory"""
|
||||
if request.method == 'POST':
|
||||
product_id = request.POST.get('product')
|
||||
warehouse_id = request.POST.get('warehouse')
|
||||
quantity_str = request.POST.get('quantity', '0').replace(',', '.')
|
||||
|
||||
try:
|
||||
product = Product.objects.get(id=product_id)
|
||||
warehouse = Warehouse.objects.get(id=warehouse_id)
|
||||
quantity = Decimal(quantity_str)
|
||||
|
||||
if quantity <= 0:
|
||||
messages.error(request, 'Quantity must be greater than 0')
|
||||
return redirect('inventory:stock_out')
|
||||
|
||||
# Check if sufficient stock is available
|
||||
inventory = Inventory.objects.filter(
|
||||
product=product, warehouse=warehouse
|
||||
).first()
|
||||
|
||||
if not inventory or inventory.quantity < quantity:
|
||||
messages.error(request, f'Insufficient stock. Available: {inventory.quantity if inventory else 0} {product.unit_of_measure.abbreviation}')
|
||||
return redirect('inventory:stock_out')
|
||||
|
||||
# Update inventory
|
||||
inventory.quantity -= quantity
|
||||
inventory.save()
|
||||
|
||||
# Create stock movement record
|
||||
StockMovement.objects.create(
|
||||
product=product,
|
||||
warehouse=warehouse,
|
||||
movement_type='out',
|
||||
quantity=quantity,
|
||||
reference_number=f"STKOUT-{timezone.now().strftime('%Y%m%d-%H%M%S')}",
|
||||
notes=request.POST.get('notes', ''),
|
||||
created_by=request.user
|
||||
)
|
||||
|
||||
messages.success(request, f'Successfully removed {quantity} {product.unit_of_measure.abbreviation} of {product.name} from {warehouse.name}')
|
||||
return redirect('inventory:movement_list')
|
||||
|
||||
except (Product.DoesNotExist, Warehouse.DoesNotExist):
|
||||
messages.error(request, 'Invalid product or warehouse selected')
|
||||
except (ValueError, InvalidOperation):
|
||||
messages.error(request, 'Invalid quantity entered')
|
||||
|
||||
# GET request - show form
|
||||
context = {
|
||||
'module_title': 'Stock Out',
|
||||
'products': Product.objects.filter(is_active=True),
|
||||
'warehouses': Warehouse.objects.filter(is_active=True),
|
||||
}
|
||||
return render(request, 'inventory/stock_out.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.add_stockmovement', raise_exception=True)
|
||||
def stock_adjustment_view(request):
|
||||
"""Adjust stock quantity"""
|
||||
# In a real app, this would contain stock adjustment logic
|
||||
return HttpResponse("Stock adjustment page")
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('inventory.add_stockmovement', raise_exception=True)
|
||||
def stock_transfer_view(request):
|
||||
"""Transfer stock between warehouses"""
|
||||
if request.method == 'POST':
|
||||
product_id = request.POST.get('product')
|
||||
from_warehouse_id = request.POST.get('from_warehouse')
|
||||
to_warehouse_id = request.POST.get('to_warehouse')
|
||||
quantity_str = request.POST.get('quantity', '0').replace(',', '.')
|
||||
|
||||
try:
|
||||
product = Product.objects.get(id=product_id)
|
||||
from_warehouse = Warehouse.objects.get(id=from_warehouse_id)
|
||||
to_warehouse = Warehouse.objects.get(id=to_warehouse_id)
|
||||
quantity = Decimal(quantity_str)
|
||||
|
||||
if quantity <= 0:
|
||||
messages.error(request, 'Quantity must be greater than 0')
|
||||
return redirect('inventory:stock_transfer')
|
||||
|
||||
if from_warehouse == to_warehouse:
|
||||
messages.error(request, 'Source and destination warehouses must be different')
|
||||
return redirect('inventory:stock_transfer')
|
||||
|
||||
# Check if sufficient stock is available in source warehouse
|
||||
from_inventory = Inventory.objects.filter(
|
||||
product=product, warehouse=from_warehouse
|
||||
).first()
|
||||
|
||||
if not from_inventory or from_inventory.quantity < quantity:
|
||||
messages.error(request, f'Insufficient stock in {from_warehouse.name}. Available: {from_inventory.quantity if from_inventory else 0} {product.unit_of_measure.abbreviation}')
|
||||
return redirect('inventory:stock_transfer')
|
||||
|
||||
# Remove from source warehouse
|
||||
from_inventory.quantity -= quantity
|
||||
from_inventory.save()
|
||||
|
||||
# Add to destination warehouse
|
||||
to_inventory, created = Inventory.objects.get_or_create(
|
||||
product=product,
|
||||
warehouse=to_warehouse,
|
||||
defaults={'quantity': 0}
|
||||
)
|
||||
to_inventory.quantity += quantity
|
||||
to_inventory.save()
|
||||
|
||||
# Create stock movement records
|
||||
reference_number = f"TRANSFER-{timezone.now().strftime('%Y%m%d-%H%M%S')}"
|
||||
|
||||
# Out movement from source
|
||||
StockMovement.objects.create(
|
||||
product=product,
|
||||
warehouse=from_warehouse,
|
||||
movement_type='out',
|
||||
quantity=quantity,
|
||||
reference_number=reference_number,
|
||||
notes=f"Transfer to {to_warehouse.name}: {request.POST.get('notes', '')}",
|
||||
created_by=request.user
|
||||
)
|
||||
|
||||
# In movement to destination
|
||||
StockMovement.objects.create(
|
||||
product=product,
|
||||
warehouse=to_warehouse,
|
||||
movement_type='in',
|
||||
quantity=quantity,
|
||||
reference_number=reference_number,
|
||||
notes=f"Transfer from {from_warehouse.name}: {request.POST.get('notes', '')}",
|
||||
created_by=request.user
|
||||
)
|
||||
|
||||
messages.success(request, f'Successfully transferred {quantity} {product.unit_of_measure.abbreviation} of {product.name} from {from_warehouse.name} to {to_warehouse.name}')
|
||||
return redirect('inventory:movement_list')
|
||||
|
||||
except (Product.DoesNotExist, Warehouse.DoesNotExist):
|
||||
messages.error(request, 'Invalid product or warehouse selected')
|
||||
except (ValueError, InvalidOperation):
|
||||
messages.error(request, 'Invalid quantity entered')
|
||||
|
||||
# GET request - show form
|
||||
context = {
|
||||
'module_title': 'Stock Transfer',
|
||||
'products': Product.objects.filter(is_active=True),
|
||||
'warehouses': Warehouse.objects.filter(is_active=True),
|
||||
}
|
||||
return render(request, 'inventory/stock_transfer.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def product_api_view(request):
|
||||
"""API endpoint to get product data with unit of measure information"""
|
||||
products = Product.objects.select_related('unit_of_measure').values(
|
||||
'id', 'name', 'unit_of_measure__id', 'unit_of_measure__name'
|
||||
)
|
||||
product_list = []
|
||||
for product in products:
|
||||
product_list.append({
|
||||
'id': product['id'],
|
||||
'name': product['name'],
|
||||
'unit_of_measure': {
|
||||
'id': product['unit_of_measure__id'],
|
||||
'name': product['unit_of_measure__name']
|
||||
} if product['unit_of_measure__id'] else None
|
||||
})
|
||||
return JsonResponse(product_list, safe=False)
|
||||
0
apps/manufacturing/__init__.py
Normal file
0
apps/manufacturing/__init__.py
Normal file
BIN
apps/manufacturing/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/manufacturing/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/manufacturing/__pycache__/admin.cpython-311.pyc
Normal file
BIN
apps/manufacturing/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/manufacturing/__pycache__/apps.cpython-311.pyc
Normal file
BIN
apps/manufacturing/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/manufacturing/__pycache__/models.cpython-311.pyc
Normal file
BIN
apps/manufacturing/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/manufacturing/__pycache__/urls.cpython-311.pyc
Normal file
BIN
apps/manufacturing/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/manufacturing/__pycache__/views.cpython-311.pyc
Normal file
BIN
apps/manufacturing/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
3
apps/manufacturing/admin.py
Normal file
3
apps/manufacturing/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
apps/manufacturing/apps.py
Normal file
6
apps/manufacturing/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ManufacturingConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.manufacturing"
|
||||
179
apps/manufacturing/migrations/0001_initial.py
Normal file
179
apps/manufacturing/migrations/0001_initial.py
Normal file
@ -0,0 +1,179 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 02:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("inventory", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="BillOfMaterial",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("bom_code", models.CharField(max_length=50, unique=True)),
|
||||
("version", models.CharField(default="1.0", max_length=20)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="boms",
|
||||
to="inventory.product",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ManufacturingOrder",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("mo_number", models.CharField(max_length=50, unique=True)),
|
||||
(
|
||||
"quantity_to_produce",
|
||||
models.DecimalField(decimal_places=2, max_digits=10),
|
||||
),
|
||||
("scheduled_start_date", models.DateField()),
|
||||
("scheduled_end_date", models.DateField()),
|
||||
("actual_start_date", models.DateField(blank=True, null=True)),
|
||||
("actual_end_date", models.DateField(blank=True, null=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("in_progress", "In Progress"),
|
||||
("completed", "Completed"),
|
||||
("cancelled", "Cancelled"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"bom",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="manufacturing.billofmaterial",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="MOComponent",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"required_quantity",
|
||||
models.DecimalField(decimal_places=2, max_digits=10),
|
||||
),
|
||||
(
|
||||
"consumed_quantity",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||
),
|
||||
(
|
||||
"component",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="inventory.product",
|
||||
),
|
||||
),
|
||||
(
|
||||
"mo",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="components",
|
||||
to="manufacturing.manufacturingorder",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("mo", "component")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="BOMItem",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("quantity", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
(
|
||||
"bom",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="items",
|
||||
to="manufacturing.billofmaterial",
|
||||
),
|
||||
),
|
||||
(
|
||||
"component",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="bom_components",
|
||||
to="inventory.product",
|
||||
),
|
||||
),
|
||||
(
|
||||
"unit_of_measure",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="inventory.unitofmeasure",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("bom", "component")},
|
||||
},
|
||||
),
|
||||
]
|
||||
12
apps/manufacturing/migrations/0002_auto_20250821_2033.py
Normal file
12
apps/manufacturing/migrations/0002_auto_20250821_2033.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 13:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("manufacturing", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = []
|
||||
@ -0,0 +1,57 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 13:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("inventory", "0001_initial"),
|
||||
("manufacturing", "0002_auto_20250821_2033"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="mocomponent",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="mocomponent",
|
||||
name="manufacturing_order",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="mocomponents",
|
||||
to="manufacturing.manufacturingorder",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="billofmaterial",
|
||||
name="bom_code",
|
||||
field=models.CharField(blank=True, max_length=50, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="manufacturingorder",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("scheduled", "Scheduled"),
|
||||
("in_progress", "In Progress"),
|
||||
("completed", "Completed"),
|
||||
("cancelled", "Cancelled"),
|
||||
],
|
||||
default="in_progress",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="mocomponent",
|
||||
unique_together={("manufacturing_order", "component")},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="mocomponent",
|
||||
name="mo",
|
||||
),
|
||||
]
|
||||
0
apps/manufacturing/migrations/__init__.py
Normal file
0
apps/manufacturing/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
117
apps/manufacturing/models.py
Normal file
117
apps/manufacturing/models.py
Normal file
@ -0,0 +1,117 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class BillOfMaterial(models.Model):
|
||||
product = models.ForeignKey('inventory.Product', on_delete=models.CASCADE, related_name='boms')
|
||||
bom_code = models.CharField(max_length=50, unique=True, blank=True)
|
||||
version = models.CharField(max_length=20, default='1.0')
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.bom_code:
|
||||
self.bom_code = self.generate_bom_code()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def generate_bom_code(self):
|
||||
"""Generate a unique BOM code"""
|
||||
last_bom = BillOfMaterial.objects.order_by('-id').first()
|
||||
|
||||
if last_bom and last_bom.bom_code and last_bom.bom_code.startswith('BOM'):
|
||||
try:
|
||||
last_number = int(last_bom.bom_code[3:]) # Remove 'BOM' prefix
|
||||
new_number = last_number + 1
|
||||
except ValueError:
|
||||
new_number = 1
|
||||
else:
|
||||
new_number = 1
|
||||
|
||||
return f'BOM{new_number:04d}'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.bom_code} - {self.product.name}"
|
||||
|
||||
@property
|
||||
def total_cost(self):
|
||||
"""Calculate total cost of all BOM items"""
|
||||
return sum(item.total_cost for item in self.items.all())
|
||||
|
||||
|
||||
class BOMItem(models.Model):
|
||||
bom = models.ForeignKey(BillOfMaterial, on_delete=models.CASCADE, related_name='items')
|
||||
component = models.ForeignKey('inventory.Product', on_delete=models.CASCADE, related_name='bom_components')
|
||||
quantity = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
unit_of_measure = models.ForeignKey('inventory.UnitOfMeasure', on_delete=models.SET_NULL, null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('bom', 'component')
|
||||
|
||||
@property
|
||||
def total_cost(self):
|
||||
"""Calculate total cost for this BOM item"""
|
||||
return self.quantity * self.component.cost
|
||||
|
||||
|
||||
class ManufacturingOrder(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('scheduled', 'Scheduled'),
|
||||
('in_progress', 'In Progress'),
|
||||
('completed', 'Completed'),
|
||||
('cancelled', 'Cancelled'),
|
||||
]
|
||||
|
||||
mo_number = models.CharField(max_length=50, unique=True)
|
||||
bom = models.ForeignKey(BillOfMaterial, on_delete=models.CASCADE)
|
||||
quantity_to_produce = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
scheduled_start_date = models.DateField()
|
||||
scheduled_end_date = models.DateField()
|
||||
actual_start_date = models.DateField(null=True, blank=True)
|
||||
actual_end_date = models.DateField(null=True, blank=True)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='in_progress')
|
||||
created_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.mo_number
|
||||
|
||||
@property
|
||||
def total_cost(self):
|
||||
"""Calculate total cost of all components"""
|
||||
return sum(component.total_cost for component in self.mocomponents.all())
|
||||
|
||||
@property
|
||||
def is_overdue(self):
|
||||
"""Check if the manufacturing order is overdue"""
|
||||
from django.utils import timezone
|
||||
if self.status in ['scheduled', 'in_progress']:
|
||||
return timezone.now().date() > self.scheduled_end_date
|
||||
return False
|
||||
|
||||
def can_start(self):
|
||||
"""Check if the manufacturing order can be started"""
|
||||
return self.status == 'scheduled'
|
||||
|
||||
def can_complete(self):
|
||||
"""Check if the manufacturing order can be completed"""
|
||||
return self.status == 'in_progress'
|
||||
|
||||
def can_cancel(self):
|
||||
"""Check if the manufacturing order can be cancelled"""
|
||||
return self.status in ['scheduled', 'in_progress']
|
||||
|
||||
|
||||
class MOComponent(models.Model):
|
||||
manufacturing_order = models.ForeignKey(ManufacturingOrder, on_delete=models.CASCADE, related_name='mocomponents', null=True, blank=True)
|
||||
component = models.ForeignKey('inventory.Product', on_delete=models.CASCADE)
|
||||
required_quantity = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
consumed_quantity = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('manufacturing_order', 'component')
|
||||
|
||||
@property
|
||||
def total_cost(self):
|
||||
"""Calculate total cost for this component"""
|
||||
return self.required_quantity * self.component.cost
|
||||
3
apps/manufacturing/tests.py
Normal file
3
apps/manufacturing/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
25
apps/manufacturing/urls.py
Normal file
25
apps/manufacturing/urls.py
Normal file
@ -0,0 +1,25 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'manufacturing'
|
||||
urlpatterns = [
|
||||
# Dashboard
|
||||
path('', views.manufacturing_dashboard, name='dashboard'),
|
||||
|
||||
# Bill of Materials
|
||||
path('bom/', views.bom_list_view, name='bom_list'),
|
||||
path('bom/create/', views.create_bom_view, name='create_bom'),
|
||||
path('bom/<int:bom_id>/', views.bom_detail_view, name='bom_detail'),
|
||||
path('bom/<int:bom_id>/edit/', views.edit_bom_view, name='edit_bom'),
|
||||
path('bom/<int:bom_id>/delete/', views.delete_bom_view, name='delete_bom'),
|
||||
|
||||
# Manufacturing Orders
|
||||
path('orders/', views.mo_list_view, name='mo_list'),
|
||||
path('orders/create/', views.create_mo_view, name='create_mo'),
|
||||
path('orders/<str:mo_number>/', views.mo_detail_view, name='mo_detail'),
|
||||
path('orders/<str:mo_number>/edit/', views.edit_mo_view, name='edit_mo'),
|
||||
path('orders/<str:mo_number>/start/', views.start_mo_view, name='start_mo'),
|
||||
path('orders/<str:mo_number>/complete/', views.complete_mo_view, name='complete_mo'),
|
||||
path('orders/<str:mo_number>/cancel/', views.cancel_mo_view, name='cancel_mo'),
|
||||
path('orders/<str:mo_number>/delete/', views.delete_mo_view, name='delete_mo'),
|
||||
]
|
||||
441
apps/manufacturing/views.py
Normal file
441
apps/manufacturing/views.py
Normal file
@ -0,0 +1,441 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.db.models import Count, Q, Sum
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from django import forms
|
||||
from django.forms import inlineformset_factory
|
||||
from decimal import Decimal
|
||||
from .models import BillOfMaterial, BOMItem, ManufacturingOrder, MOComponent
|
||||
from apps.inventory.models import Product
|
||||
|
||||
|
||||
class BOMItemForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BOMItem
|
||||
fields = ['component', 'quantity', 'unit_of_measure']
|
||||
widgets = {
|
||||
'quantity': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}),
|
||||
}
|
||||
|
||||
|
||||
class BOMForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = BillOfMaterial
|
||||
fields = ['product', 'version', 'is_active']
|
||||
widgets = {
|
||||
'version': forms.TextInput(attrs={'placeholder': '1.0'}),
|
||||
}
|
||||
|
||||
|
||||
BOMItemFormSet = inlineformset_factory(
|
||||
BillOfMaterial,
|
||||
BOMItem,
|
||||
form=BOMItemForm,
|
||||
extra=1,
|
||||
can_delete=True,
|
||||
min_num=1,
|
||||
validate_min=True
|
||||
)
|
||||
|
||||
|
||||
class MOComponentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = MOComponent
|
||||
fields = ['component', 'required_quantity']
|
||||
widgets = {
|
||||
'required_quantity': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}),
|
||||
}
|
||||
|
||||
|
||||
class MOForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ManufacturingOrder
|
||||
fields = ['mo_number', 'bom', 'quantity_to_produce', 'scheduled_start_date', 'scheduled_end_date']
|
||||
widgets = {
|
||||
'scheduled_start_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'scheduled_end_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'quantity_to_produce': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}),
|
||||
}
|
||||
|
||||
|
||||
MOComponentFormSet = inlineformset_factory(
|
||||
ManufacturingOrder,
|
||||
MOComponent,
|
||||
form=MOComponentForm,
|
||||
extra=0,
|
||||
can_delete=True,
|
||||
min_num=0
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def manufacturing_dashboard(request):
|
||||
"""Manufacturing dashboard view"""
|
||||
total_boms = BillOfMaterial.objects.filter(is_active=True)
|
||||
active_orders = ManufacturingOrder.objects.filter(
|
||||
Q(status='in_progress') | Q(status='scheduled')
|
||||
)
|
||||
completed_orders = ManufacturingOrder.objects.filter(
|
||||
status='completed',
|
||||
actual_end_date__gte=timezone.now() - timedelta(days=30)
|
||||
)
|
||||
|
||||
recent_orders = ManufacturingOrder.objects.select_related('bom__product').order_by('-created_at')[:5]
|
||||
|
||||
context = {
|
||||
'module_title': 'Manufacturing Management',
|
||||
'total_boms': total_boms,
|
||||
'active_orders': active_orders,
|
||||
'completed_orders': completed_orders,
|
||||
'recent_orders': recent_orders,
|
||||
}
|
||||
return render(request, 'manufacturing/dashboard.html', context)
|
||||
|
||||
|
||||
# Bill of Material Views
|
||||
@login_required
|
||||
@permission_required('manufacturing.view_billofmaterial', raise_exception=True)
|
||||
def bom_list_view(request):
|
||||
"""List all bills of materials"""
|
||||
boms = BillOfMaterial.objects.all()
|
||||
context = {
|
||||
'module_title': 'Bill of Materials List',
|
||||
'boms': boms,
|
||||
}
|
||||
return render(request, 'manufacturing/bom_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.add_billofmaterial', raise_exception=True)
|
||||
def create_bom_view(request):
|
||||
"""Create a new bill of material"""
|
||||
if request.method == 'POST':
|
||||
form = BOMForm(request.POST)
|
||||
formset = BOMItemFormSet(request.POST)
|
||||
|
||||
if form.is_valid() and formset.is_valid():
|
||||
# Save the BOM
|
||||
bom = form.save(commit=False)
|
||||
bom.save()
|
||||
|
||||
# Save the BOM items
|
||||
items = formset.save(commit=False)
|
||||
for item in items:
|
||||
item.bom = bom
|
||||
item.save()
|
||||
|
||||
messages.success(request, f'Bill of Material "{bom.bom_code}" created successfully!')
|
||||
return redirect('manufacturing:bom_detail', bom_id=bom.id)
|
||||
else:
|
||||
form = BOMForm()
|
||||
formset = BOMItemFormSet()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'formset': formset,
|
||||
'module_title': 'Create Bill of Material',
|
||||
'is_create': True,
|
||||
}
|
||||
return render(request, 'manufacturing/bom_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.view_billofmaterial', raise_exception=True)
|
||||
def bom_detail_view(request, bom_id):
|
||||
"""View bill of material details"""
|
||||
try:
|
||||
bom = BillOfMaterial.objects.get(id=bom_id)
|
||||
context = {
|
||||
'module_title': 'Bill of Material Details',
|
||||
'bom': bom,
|
||||
}
|
||||
return render(request, 'manufacturing/bom_detail.html', context)
|
||||
except BillOfMaterial.DoesNotExist:
|
||||
messages.error(request, 'Bill of material not found')
|
||||
return redirect('manufacturing:bom_list')
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.change_billofmaterial', raise_exception=True)
|
||||
def edit_bom_view(request, bom_id):
|
||||
"""Edit an existing bill of material"""
|
||||
try:
|
||||
bom = get_object_or_404(BillOfMaterial, id=bom_id)
|
||||
except:
|
||||
messages.error(request, 'Bill of Material not found.')
|
||||
return redirect('manufacturing:bom_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = BOMForm(request.POST, instance=bom)
|
||||
formset = BOMItemFormSet(request.POST, instance=bom)
|
||||
|
||||
if form.is_valid() and formset.is_valid():
|
||||
# Save the BOM
|
||||
bom = form.save(commit=False)
|
||||
bom.save()
|
||||
|
||||
# Delete existing items and save new ones
|
||||
BOMItem.objects.filter(bom=bom).delete()
|
||||
items = formset.save(commit=False)
|
||||
for item in items:
|
||||
item.bom = bom
|
||||
item.save()
|
||||
|
||||
messages.success(request, f'Bill of Material "{bom.bom_code}" updated successfully!')
|
||||
return redirect('manufacturing:bom_detail', bom_id=bom.id)
|
||||
else:
|
||||
form = BOMForm(instance=bom)
|
||||
formset = BOMItemFormSet(instance=bom)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'formset': formset,
|
||||
'bom': bom,
|
||||
'module_title': f'Edit Bill of Material: {bom.bom_code}',
|
||||
'is_create': False,
|
||||
}
|
||||
return render(request, 'manufacturing/bom_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.delete_billofmaterial', raise_exception=True)
|
||||
def delete_bom_view(request, bom_id):
|
||||
"""Delete a bill of material"""
|
||||
try:
|
||||
bom = get_object_or_404(BillOfMaterial, id=bom_id)
|
||||
except:
|
||||
messages.error(request, 'Bill of Material not found.')
|
||||
return redirect('manufacturing:bom_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
bom_code = bom.bom_code
|
||||
bom.delete()
|
||||
messages.success(request, f'Bill of Material "{bom_code}" deleted successfully!')
|
||||
return redirect('manufacturing:bom_list')
|
||||
|
||||
context = {
|
||||
'bom': bom,
|
||||
'module_title': f'Delete Bill of Material: {bom.bom_code}',
|
||||
}
|
||||
return render(request, 'manufacturing/bom_confirm_delete.html', context)
|
||||
|
||||
|
||||
# Manufacturing Order Views
|
||||
@login_required
|
||||
@permission_required('manufacturing.view_manufacturingorder', raise_exception=True)
|
||||
def mo_list_view(request):
|
||||
"""List all manufacturing orders"""
|
||||
mos = ManufacturingOrder.objects.all()
|
||||
context = {
|
||||
'module_title': 'Manufacturing Order List',
|
||||
'mos': mos,
|
||||
}
|
||||
return render(request, 'manufacturing/mo_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.add_manufacturingorder', raise_exception=True)
|
||||
def create_mo_view(request):
|
||||
"""Create a new manufacturing order"""
|
||||
if request.method == 'POST':
|
||||
form = MOForm(request.POST)
|
||||
formset = MOComponentFormSet(request.POST)
|
||||
|
||||
if form.is_valid() and formset.is_valid():
|
||||
# Save the MO
|
||||
mo = form.save(commit=False)
|
||||
mo.save()
|
||||
|
||||
# Save the MO components
|
||||
components = formset.save(commit=False)
|
||||
for component in components:
|
||||
component.manufacturing_order = mo
|
||||
component.save()
|
||||
|
||||
messages.success(request, f'Manufacturing Order "{mo.mo_number}" created successfully!')
|
||||
return redirect('manufacturing:mo_detail', mo_number=mo.mo_number)
|
||||
else:
|
||||
form = MOForm()
|
||||
formset = MOComponentFormSet()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'formset': formset,
|
||||
'module_title': 'Create Manufacturing Order',
|
||||
'is_create': True,
|
||||
}
|
||||
return render(request, 'manufacturing/mo_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.view_manufacturingorder', raise_exception=True)
|
||||
def mo_detail_view(request, mo_number):
|
||||
"""View manufacturing order details"""
|
||||
try:
|
||||
mo = ManufacturingOrder.objects.get(mo_number=mo_number)
|
||||
context = {
|
||||
'module_title': 'Manufacturing Order Details',
|
||||
'mo': mo,
|
||||
}
|
||||
return render(request, 'manufacturing/mo_detail.html', context)
|
||||
except ManufacturingOrder.DoesNotExist:
|
||||
messages.error(request, 'Manufacturing order not found')
|
||||
return redirect('manufacturing:mo_list')
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.change_manufacturingorder', raise_exception=True)
|
||||
def edit_mo_view(request, mo_number):
|
||||
"""Edit an existing manufacturing order"""
|
||||
try:
|
||||
mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number)
|
||||
except:
|
||||
messages.error(request, 'Manufacturing Order not found.')
|
||||
return redirect('manufacturing:mo_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = MOForm(request.POST, instance=mo)
|
||||
formset = MOComponentFormSet(request.POST, instance=mo)
|
||||
|
||||
if form.is_valid() and formset.is_valid():
|
||||
# Save the MO
|
||||
mo = form.save(commit=False)
|
||||
mo.save()
|
||||
|
||||
# Delete existing components and save new ones
|
||||
MOComponent.objects.filter(manufacturing_order=mo).delete()
|
||||
components = formset.save(commit=False)
|
||||
for component in components:
|
||||
component.manufacturing_order = mo
|
||||
component.save()
|
||||
|
||||
messages.success(request, f'Manufacturing Order "{mo.mo_number}" updated successfully!')
|
||||
return redirect('manufacturing:mo_detail', mo_number=mo.mo_number)
|
||||
else:
|
||||
form = MOForm(instance=mo)
|
||||
formset = MOComponentFormSet(instance=mo)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'formset': formset,
|
||||
'mo': mo,
|
||||
'module_title': f'Edit Manufacturing Order: {mo.mo_number}',
|
||||
'is_create': False,
|
||||
}
|
||||
return render(request, 'manufacturing/mo_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.change_manufacturingorder', raise_exception=True)
|
||||
def start_mo_view(request, mo_number):
|
||||
"""Start a manufacturing order"""
|
||||
try:
|
||||
mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number)
|
||||
except:
|
||||
messages.error(request, 'Manufacturing Order not found.')
|
||||
return redirect('manufacturing:mo_list')
|
||||
|
||||
if mo.status != 'scheduled':
|
||||
messages.error(request, f'Cannot start Manufacturing Order in {mo.get_status_display()} status.')
|
||||
return redirect('manufacturing:mo_detail', mo_number=mo.mo_number)
|
||||
|
||||
if request.method == 'POST':
|
||||
mo.status = 'in_progress'
|
||||
mo.actual_start_date = timezone.now()
|
||||
mo.save()
|
||||
|
||||
messages.success(request, f'Manufacturing Order "{mo.mo_number}" started successfully!')
|
||||
return redirect('manufacturing:mo_detail', mo_number=mo.mo_number)
|
||||
|
||||
context = {
|
||||
'mo': mo,
|
||||
'module_title': f'Start Manufacturing Order: {mo.mo_number}',
|
||||
'action': 'Start',
|
||||
}
|
||||
return render(request, 'manufacturing/mo_confirm_action.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.change_manufacturingorder', raise_exception=True)
|
||||
def complete_mo_view(request, mo_number):
|
||||
"""Complete a manufacturing order"""
|
||||
try:
|
||||
mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number)
|
||||
except:
|
||||
messages.error(request, 'Manufacturing Order not found.')
|
||||
return redirect('manufacturing:mo_list')
|
||||
|
||||
if mo.status != 'in_progress':
|
||||
messages.error(request, f'Cannot complete Manufacturing Order in {mo.get_status_display()} status.')
|
||||
return redirect('manufacturing:mo_detail', mo_number=mo.mo_number)
|
||||
|
||||
if request.method == 'POST':
|
||||
mo.status = 'completed'
|
||||
mo.actual_end_date = timezone.now().date() # Using date() since actual_end_date is DateField
|
||||
mo.save()
|
||||
|
||||
messages.success(request, f'Manufacturing Order "{mo.mo_number}" completed successfully!')
|
||||
return redirect('manufacturing:mo_detail', mo_number=mo.mo_number)
|
||||
|
||||
context = {
|
||||
'mo': mo,
|
||||
'module_title': f'Complete Manufacturing Order: {mo.mo_number}',
|
||||
'action': 'Complete',
|
||||
}
|
||||
return render(request, 'manufacturing/mo_confirm_action.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.change_manufacturingorder', raise_exception=True)
|
||||
def cancel_mo_view(request, mo_number):
|
||||
"""Cancel a manufacturing order"""
|
||||
try:
|
||||
mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number)
|
||||
except:
|
||||
messages.error(request, 'Manufacturing Order not found.')
|
||||
return redirect('manufacturing:mo_list')
|
||||
|
||||
if mo.status in ['completed', 'cancelled']:
|
||||
messages.error(request, f'Cannot cancel Manufacturing Order in {mo.get_status_display()} status.')
|
||||
return redirect('manufacturing:mo_detail', mo_number=mo.mo_number)
|
||||
|
||||
if request.method == 'POST':
|
||||
mo.status = 'cancelled'
|
||||
mo.actual_end_date = timezone.now()
|
||||
mo.save()
|
||||
|
||||
messages.success(request, f'Manufacturing Order "{mo.mo_number}" cancelled successfully!')
|
||||
return redirect('manufacturing:mo_detail', mo_number=mo.mo_number)
|
||||
|
||||
context = {
|
||||
'mo': mo,
|
||||
'module_title': f'Cancel Manufacturing Order: {mo.mo_number}',
|
||||
'action': 'Cancel',
|
||||
}
|
||||
return render(request, 'manufacturing/mo_confirm_action.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('manufacturing.delete_manufacturingorder', raise_exception=True)
|
||||
def delete_mo_view(request, mo_number):
|
||||
"""Delete a manufacturing order"""
|
||||
try:
|
||||
mo = get_object_or_404(ManufacturingOrder, mo_number=mo_number)
|
||||
except:
|
||||
messages.error(request, 'Manufacturing Order not found.')
|
||||
return redirect('manufacturing:mo_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
mo_number = mo.mo_number
|
||||
mo.delete()
|
||||
messages.success(request, f'Manufacturing Order "{mo_number}" deleted successfully!')
|
||||
return redirect('manufacturing:mo_list')
|
||||
|
||||
context = {
|
||||
'mo': mo,
|
||||
'module_title': f'Delete Manufacturing Order: {mo.mo_number}',
|
||||
}
|
||||
return render(request, 'manufacturing/mo_confirm_delete.html', context)
|
||||
0
apps/purchasing/__init__.py
Normal file
0
apps/purchasing/__init__.py
Normal file
BIN
apps/purchasing/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/purchasing/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/purchasing/__pycache__/admin.cpython-311.pyc
Normal file
BIN
apps/purchasing/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/purchasing/__pycache__/apps.cpython-311.pyc
Normal file
BIN
apps/purchasing/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/purchasing/__pycache__/models.cpython-311.pyc
Normal file
BIN
apps/purchasing/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/purchasing/__pycache__/urls.cpython-311.pyc
Normal file
BIN
apps/purchasing/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
apps/purchasing/__pycache__/views.cpython-311.pyc
Normal file
BIN
apps/purchasing/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
3
apps/purchasing/admin.py
Normal file
3
apps/purchasing/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
apps/purchasing/apps.py
Normal file
6
apps/purchasing/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PurchasingConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.purchasing"
|
||||
207
apps/purchasing/migrations/0001_initial.py
Normal file
207
apps/purchasing/migrations/0001_initial.py
Normal file
@ -0,0 +1,207 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 02:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("inventory", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GoodsReceipt",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("gr_number", models.CharField(max_length=50, unique=True)),
|
||||
("receipt_date", models.DateField()),
|
||||
("notes", models.TextField(blank=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PurchaseOrder",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("po_number", models.CharField(max_length=50, unique=True)),
|
||||
("order_date", models.DateField()),
|
||||
("expected_delivery_date", models.DateField(blank=True, null=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("ordered", "Ordered"),
|
||||
("completed", "Completed"),
|
||||
("cancelled", "Cancelled"),
|
||||
],
|
||||
default="ordered",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"subtotal",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=12),
|
||||
),
|
||||
(
|
||||
"tax_amount",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=12),
|
||||
),
|
||||
(
|
||||
"total_amount",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=12),
|
||||
),
|
||||
("notes", models.TextField(blank=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Supplier",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("code", models.CharField(max_length=50, unique=True)),
|
||||
("name", models.CharField(max_length=200)),
|
||||
("contact_person", models.CharField(blank=True, max_length=100)),
|
||||
("email", models.EmailField(blank=True, max_length=254)),
|
||||
("phone", models.CharField(blank=True, max_length=20)),
|
||||
("address", models.TextField(blank=True)),
|
||||
("tax_id", models.CharField(blank=True, max_length=50)),
|
||||
("payment_terms", models.CharField(blank=True, max_length=100)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PurchaseOrderItem",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("quantity", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
("unit_price", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
("total_price", models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
(
|
||||
"received_quantity",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||
),
|
||||
(
|
||||
"po",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="items",
|
||||
to="purchasing.purchaseorder",
|
||||
),
|
||||
),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="inventory.product",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="purchaseorder",
|
||||
name="supplier",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="purchasing.supplier"
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="GoodsReceiptItem",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"received_quantity",
|
||||
models.DecimalField(decimal_places=2, max_digits=10),
|
||||
),
|
||||
("notes", models.TextField(blank=True)),
|
||||
(
|
||||
"po_item",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="purchasing.purchaseorderitem",
|
||||
),
|
||||
),
|
||||
(
|
||||
"receipt",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="items",
|
||||
to="purchasing.goodsreceipt",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="goodsreceipt",
|
||||
name="po",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="purchasing.purchaseorder",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="goodsreceipt",
|
||||
name="received_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.23 on 2025-08-21 13:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("purchasing", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchaseorder",
|
||||
name="po_number",
|
||||
field=models.CharField(blank=True, max_length=50, unique=True),
|
||||
),
|
||||
]
|
||||
0
apps/purchasing/migrations/__init__.py
Normal file
0
apps/purchasing/migrations/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/purchasing/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
apps/purchasing/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
94
apps/purchasing/models.py
Normal file
94
apps/purchasing/models.py
Normal file
@ -0,0 +1,94 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Supplier(models.Model):
|
||||
code = models.CharField(max_length=50, unique=True)
|
||||
name = models.CharField(max_length=200)
|
||||
contact_person = models.CharField(max_length=100, blank=True)
|
||||
email = models.EmailField(blank=True)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
address = models.TextField(blank=True)
|
||||
tax_id = models.CharField(max_length=50, blank=True)
|
||||
payment_terms = models.CharField(max_length=100, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PurchaseOrder(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('ordered', 'Ordered'),
|
||||
('completed', 'Completed'),
|
||||
('cancelled', 'Cancelled'),
|
||||
]
|
||||
|
||||
po_number = models.CharField(max_length=50, unique=True, blank=True)
|
||||
supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
|
||||
order_date = models.DateField()
|
||||
expected_delivery_date = models.DateField(null=True, blank=True)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ordered')
|
||||
subtotal = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
tax_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
notes = models.TextField(blank=True)
|
||||
created_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.po_number:
|
||||
self.po_number = self.generate_po_number()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def generate_po_number(self):
|
||||
"""Generate a unique purchase order number"""
|
||||
last_po = PurchaseOrder.objects.order_by('-id').first()
|
||||
|
||||
if last_po and last_po.po_number and last_po.po_number.startswith('PO'):
|
||||
try:
|
||||
last_number = int(last_po.po_number[2:]) # Remove 'PO' prefix
|
||||
new_number = last_number + 1
|
||||
except ValueError:
|
||||
new_number = 1
|
||||
else:
|
||||
new_number = 1
|
||||
|
||||
return f'PO{new_number:04d}'
|
||||
|
||||
def __str__(self):
|
||||
return self.po_number
|
||||
|
||||
|
||||
class PurchaseOrderItem(models.Model):
|
||||
po = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE, related_name='items')
|
||||
product = models.ForeignKey('inventory.Product', on_delete=models.CASCADE)
|
||||
quantity = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
total_price = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
received_quantity = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.total_price = self.quantity * self.unit_price
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class GoodsReceipt(models.Model):
|
||||
gr_number = models.CharField(max_length=50, unique=True)
|
||||
po = models.ForeignKey(PurchaseOrder, on_delete=models.CASCADE)
|
||||
receipt_date = models.DateField()
|
||||
received_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True)
|
||||
notes = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.gr_number
|
||||
|
||||
|
||||
class GoodsReceiptItem(models.Model):
|
||||
receipt = models.ForeignKey(GoodsReceipt, on_delete=models.CASCADE, related_name='items')
|
||||
po_item = models.ForeignKey(PurchaseOrderItem, on_delete=models.CASCADE)
|
||||
received_quantity = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
notes = models.TextField(blank=True)
|
||||
3
apps/purchasing/tests.py
Normal file
3
apps/purchasing/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user