diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..85f944e8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: Django CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: imaps_test + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h localhost -ppassword" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: 3.12 + + - name: Install system deps for MySQL client + run: | + sudo apt-get update + sudo apt-get install -y default-libmysqlclient-dev build-essential + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run migrations + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_NAME: imaps + DB_USER: root + DB_PASS: password + run: | + python manage.py migrate --noinput + + - name: Run tests + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_NAME: imaps_test + DB_USER: root + DB_PASS: password + run: | + python manage.py test --verbosity=2 diff --git a/IMAPS/IMAPS/.gitignore b/IMAPS/IMAPS/.gitignore new file mode 100644 index 00000000..42ebefc0 --- /dev/null +++ b/IMAPS/IMAPS/.gitignore @@ -0,0 +1,6 @@ +# Ignore OS-generated files +.DS_Store +Thumbs.db +.env +# Ignore dependencies +venv/ diff --git a/IMAPS/IMAPS/IMAPS/__init__.py b/IMAPS/IMAPS/IMAPS/__init__.py index e69de29b..063cd2cc 100644 --- a/IMAPS/IMAPS/IMAPS/__init__.py +++ b/IMAPS/IMAPS/IMAPS/__init__.py @@ -0,0 +1,2 @@ +import pymysql +pymysql.install_as_MySQLdb() diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-311.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 7c8657ef..00000000 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-312.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-312.pyc index 750d5d14..c60c8c89 100644 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-39.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 8d54f18c..00000000 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-311.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-311.pyc deleted file mode 100644 index 0705ba52..00000000 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-312.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-312.pyc index c58a28d6..ccf6e391 100644 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-39.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-39.pyc deleted file mode 100644 index d6999e80..00000000 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/settings.cpython-39.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-311.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-311.pyc deleted file mode 100644 index 8c17b8f8..00000000 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-312.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-312.pyc index 82c5ef9b..2f0b211d 100644 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-39.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-39.pyc deleted file mode 100644 index bfdfb6ca..00000000 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/urls.cpython-39.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-311.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-311.pyc deleted file mode 100644 index 9863c083..00000000 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-312.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-312.pyc index a661e9b0..e77681cf 100644 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-39.pyc b/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-39.pyc deleted file mode 100644 index 984a56df..00000000 Binary files a/IMAPS/IMAPS/IMAPS/__pycache__/wsgi.cpython-39.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS/settings.py b/IMAPS/IMAPS/IMAPS/settings.py index 3b0f8e18..15956b33 100644 --- a/IMAPS/IMAPS/IMAPS/settings.py +++ b/IMAPS/IMAPS/IMAPS/settings.py @@ -11,7 +11,8 @@ """ from pathlib import Path - +import os +from dotenv import load_dotenv # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -75,20 +76,19 @@ # https://docs.djangoproject.com/en/5.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'imaps', # Your DB name - 'USER': 'root', # Your MySQL username - 'PASSWORD': '1234', # Your MySQL password - 'HOST': '127.0.0.1', # Use '127.0.0.1' instead of 'localhost' - 'PORT': '3306', # Use 3306 if running a standard MySQL server - 'OPTIONS': { - 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" - } + "default": { + "ENGINE": os.getenv("DB_ENGINE", "django.db.backends.sqlite3"), + "NAME": os.getenv("DB_NAME", BASE_DIR / "db.sqlite3"), + "USER": os.getenv("DB_USER", ""), + "PASSWORD": os.getenv("DB_PASS", ""), + "HOST": os.getenv("DB_HOST", ""), + "PORT": os.getenv("DB_PORT", ""), + "OPTIONS": {"init_command": "SET sql_mode='STRICT_TRANS_TABLES'"}, } } + # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators diff --git a/IMAPS/IMAPS/IMAPS_app/__pycache__/forms.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/__pycache__/forms.cpython-312.pyc index 652c0f86..538cffab 100644 Binary files a/IMAPS/IMAPS/IMAPS_app/__pycache__/forms.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS_app/__pycache__/forms.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS_app/__pycache__/models.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/__pycache__/models.cpython-312.pyc index 23146b24..21767875 100644 Binary files a/IMAPS/IMAPS/IMAPS_app/__pycache__/models.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS_app/__pycache__/models.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS_app/__pycache__/views.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/__pycache__/views.cpython-312.pyc index ef7d4901..20e67c3e 100644 Binary files a/IMAPS/IMAPS/IMAPS_app/__pycache__/views.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS_app/__pycache__/views.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS_app/forms.py b/IMAPS/IMAPS/IMAPS_app/forms.py index a16e7124..47b553d9 100644 --- a/IMAPS/IMAPS/IMAPS_app/forms.py +++ b/IMAPS/IMAPS/IMAPS_app/forms.py @@ -35,7 +35,7 @@ class SupplierForm(RequiredFieldsAsteriskMixin, forms.ModelForm): class Meta: model = Supplier - fields = '__all__' + exclude = ['change_status'] class IngredientsRawMaterialsForm(RequiredFieldsAsteriskMixin, forms.ModelForm): UseCategory = forms.ChoiceField( diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/0003_ingredientsrawmaterials_change_status_and_more.py b/IMAPS/IMAPS/IMAPS_app/migrations/0003_ingredientsrawmaterials_change_status_and_more.py new file mode 100644 index 00000000..db2662c8 --- /dev/null +++ b/IMAPS/IMAPS/IMAPS_app/migrations/0003_ingredientsrawmaterials_change_status_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.1.6 on 2025-05-08 11:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('IMAPS_app', '0002_alter_ingredientsrawmaterials_cost_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ingredientsrawmaterials', + name='change_status', + field=models.CharField(choices=[('active', 'Active'), ('old_modified', 'Old Modified'), ('new_modified', 'New Modified'), ('deleted', 'Deleted')], default='active', max_length=15), + ), + migrations.AddField( + model_name='packagingrawmaterials', + name='change_status', + field=models.CharField(choices=[('active', 'Active'), ('old_modified', 'Old Modified'), ('new_modified', 'New Modified'), ('deleted', 'Deleted')], default='active', max_length=15), + ), + migrations.AddField( + model_name='supplier', + name='change_status', + field=models.CharField(choices=[('active', 'Active'), ('old_modified', 'Old Modified'), ('new_modified', 'New Modified'), ('deleted', 'Deleted')], default='active', max_length=15), + ), + migrations.AddField( + model_name='usedingredient', + name='change_status', + field=models.CharField(choices=[('active', 'Active'), ('old_modified', 'Old Modified'), ('new_modified', 'New Modified'), ('deleted', 'Deleted')], default='active', max_length=15), + ), + migrations.AddField( + model_name='usedpackaging', + name='change_status', + field=models.CharField(choices=[('active', 'Active'), ('old_modified', 'Old Modified'), ('new_modified', 'New Modified'), ('deleted', 'Deleted')], default='active', max_length=15), + ), + migrations.AlterField( + model_name='supplier', + name='SupplierCode', + field=models.CharField(max_length=50, primary_key=True, serialize=False), + ), + ] diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-311.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-311.pyc deleted file mode 100644 index f7025d1c..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-312.pyc index 225a8b8b..289e10eb 100644 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-39.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-39.pyc deleted file mode 100644 index 0951ab43..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0001_initial.cpython-39.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_alter_ingredientsrawmaterials_cost_and_more.cpython-311.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_alter_ingredientsrawmaterials_cost_and_more.cpython-311.pyc deleted file mode 100644 index ebb7dcd9..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_alter_ingredientsrawmaterials_cost_and_more.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_alter_ingredientsrawmaterials_cost_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_alter_ingredientsrawmaterials_cost_and_more.cpython-312.pyc index 0782dbd3..ba9ac272 100644 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_alter_ingredientsrawmaterials_cost_and_more.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_alter_ingredientsrawmaterials_cost_and_more.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_packagingrawmaterials_batch_code_and_more.cpython-311.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_packagingrawmaterials_batch_code_and_more.cpython-311.pyc deleted file mode 100644 index ea9705e1..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_packagingrawmaterials_batch_code_and_more.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_packagingrawmaterials_batch_code_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_packagingrawmaterials_batch_code_and_more.cpython-312.pyc deleted file mode 100644 index a41ef526..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_packagingrawmaterials_batch_code_and_more.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_supplier_contact_email_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_supplier_contact_email_and_more.cpython-312.pyc deleted file mode 100644 index f640a76e..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_remove_supplier_contact_email_and_more.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_rename_quanityused_usedpackaging_quantityused.cpython-311.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_rename_quanityused_usedpackaging_quantityused.cpython-311.pyc deleted file mode 100644 index a31da738..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0002_rename_quanityused_usedpackaging_quantityused.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_alter_packagingmaterial_batch_code_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_alter_packagingmaterial_batch_code_and_more.cpython-312.pyc deleted file mode 100644 index 64e93634..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_alter_packagingmaterial_batch_code_and_more.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_ingredientsrawmaterials_change_status_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_ingredientsrawmaterials_change_status_and_more.cpython-312.pyc new file mode 100644 index 00000000..3660ca6c Binary files /dev/null and b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_ingredientsrawmaterials_change_status_and_more.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_packagingrawmaterials_name_and_more.cpython-311.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_packagingrawmaterials_name_and_more.cpython-311.pyc deleted file mode 100644 index a9e716ae..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_packagingrawmaterials_name_and_more.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_packagingrawmaterials_name_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_packagingrawmaterials_name_and_more.cpython-312.pyc deleted file mode 100644 index b2011f36..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0003_packagingrawmaterials_name_and_more.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_alter_packagingmaterial_supplier.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_alter_packagingmaterial_supplier.cpython-312.pyc deleted file mode 100644 index 4499e993..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_alter_packagingmaterial_supplier.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_remove_packagingrawmaterials_name.cpython-311.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_remove_packagingrawmaterials_name.cpython-311.pyc deleted file mode 100644 index affd4304..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_remove_packagingrawmaterials_name.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_remove_packagingrawmaterials_name.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_remove_packagingrawmaterials_name.cpython-312.pyc deleted file mode 100644 index e37a3bba..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0004_remove_packagingrawmaterials_name.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0005_alter_ingredientsrawmaterials_rawmaterialbatchcode_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0005_alter_ingredientsrawmaterials_rawmaterialbatchcode_and_more.cpython-312.pyc deleted file mode 100644 index 5397c1ee..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0005_alter_ingredientsrawmaterials_rawmaterialbatchcode_and_more.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0005_remove_supplier_category_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0005_remove_supplier_category_and_more.cpython-312.pyc deleted file mode 100644 index af93f880..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0005_remove_supplier_category_and_more.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0006_alter_supplier_suppliercode_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0006_alter_supplier_suppliercode_and_more.cpython-312.pyc deleted file mode 100644 index 611cae83..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0006_alter_supplier_suppliercode_and_more.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0007_alter_supplier_suppliercode_and_more.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0007_alter_supplier_suppliercode_and_more.cpython-312.pyc deleted file mode 100644 index fe9a349b..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/0007_alter_supplier_suppliercode_and_more.cpython-312.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-311.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index e26f636a..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-312.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-312.pyc index 8ef14a52..2c91c8a5 100644 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-312.pyc and b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-39.pyc b/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 1a777d80..00000000 Binary files a/IMAPS/IMAPS/IMAPS_app/migrations/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/IMAPS/IMAPS/IMAPS_app/models.py b/IMAPS/IMAPS/IMAPS_app/models.py index d42d0f38..dabaa740 100644 --- a/IMAPS/IMAPS/IMAPS_app/models.py +++ b/IMAPS/IMAPS/IMAPS_app/models.py @@ -1,15 +1,19 @@ +# Updated models.py with change_status logic import random import string from datetime import date, timedelta from django.db import models +CHANGE_STATUS_CHOICES = [ + ("active", "Active"), + ("old_modified", "Old Modified"), + ("new_modified", "New Modified"), + ("deleted", "Deleted"), +] + # -------------------- Supplier -------------------- class Supplier(models.Model): - SupplierCode = models.CharField( - max_length=50, - primary_key=True, - help_text="Unique identifier for the supplier (manually given)." - ) + SupplierCode = models.CharField(max_length=50, primary_key=True) SupplierName = models.CharField(max_length=255) CATEGORY_CHOICES = [ ('Ingredient', 'Ingredient'), @@ -21,6 +25,7 @@ class Supplier(models.Model): EmailAddress = models.CharField(max_length=320, blank=True, null=True) ContactNumber = models.CharField(max_length=18, blank=True, null=True) PointPerson = models.CharField(max_length=255, blank=True, null=True) + change_status = models.CharField(max_length=15, choices=CHANGE_STATUS_CHOICES, default="active") def __str__(self): return f"{self.SupplierName} ({self.SupplierCode})" @@ -30,27 +35,19 @@ def __str__(self): class IngredientsRawMaterials(models.Model): id = models.AutoField(primary_key=True) RawMaterialBatchCode = models.CharField(max_length=50, unique=True, blank=True) - SupplierCode = models.ForeignKey( - Supplier, - on_delete=models.CASCADE, - db_column='SupplierCode' - ) + SupplierCode = models.ForeignKey(Supplier, on_delete=models.CASCADE, db_column='SupplierCode') RawMaterialName = models.CharField(max_length=255) DateDelivered = models.DateField() QuantityBought = models.FloatField(default=0) QuantityLeft = models.FloatField(default=0) - USECATEGORY_CHOICES = [ - ('WBC', 'WBC'), - ('GGB', 'GGB'), - ('Both', 'Both'), - ] + USECATEGORY_CHOICES = [('WBC', 'WBC'), ('GGB', 'GGB'), ('Both', 'Both')] UseCategory = models.CharField(max_length=10, choices=USECATEGORY_CHOICES, default="GGB") ExpirationDate = models.DateField() Status = models.CharField(max_length=15, blank=True) Cost = models.FloatField(default=0, blank=True, null=True) + change_status = models.CharField(max_length=15, choices=CHANGE_STATUS_CHOICES, default="active") def save(self, *args, **kwargs): - # Determine if we need to regenerate batch code regenerate = False if not self.pk: regenerate = True @@ -68,7 +65,6 @@ def save(self, *args, **kwargs): if self.QuantityLeft == 0: self.QuantityLeft = self.QuantityBought - # Auto-compute Status today = date.today() if self.ExpirationDate and self.ExpirationDate <= today + timedelta(days=30): self.Status = "Expiring" @@ -87,24 +83,17 @@ def __str__(self): class PackagingRawMaterials(models.Model): id = models.AutoField(primary_key=True) PackagingBatchCode = models.CharField(max_length=50, unique=True, blank=True) - SupplierCode = models.ForeignKey( - Supplier, - on_delete=models.CASCADE, - db_column='SupplierCode' - ) + SupplierCode = models.ForeignKey(Supplier, on_delete=models.CASCADE, db_column='SupplierCode') RawMaterialName = models.CharField(max_length=255) QuantityBought = models.IntegerField(default=0) QuantityLeft = models.IntegerField(default=0) - USECATEGORY_CHOICES = [ - ('WBC', 'WBC'), - ('GGB', 'GGB'), - ('Both', 'Both'), - ] + USECATEGORY_CHOICES = [('WBC', 'WBC'), ('GGB', 'GGB'), ('Both', 'Both')] UseCategory = models.CharField(max_length=10, choices=USECATEGORY_CHOICES) Status = models.CharField(max_length=15, blank=True) Cost = models.FloatField(default=0, blank=True, null=True) ContainerSize = models.CharField(max_length=50, blank=True, null=True) DateDelivered = models.DateField() + change_status = models.CharField(max_length=15, choices=CHANGE_STATUS_CHOICES, default="active") def save(self, *args, **kwargs): regenerate = False @@ -139,20 +128,13 @@ def __str__(self): # -------------------- UsedIngredient -------------------- class UsedIngredient(models.Model): UsedIngredientBatchCode = models.CharField(max_length=50, primary_key=True, blank=True) - IngredientRawMaterialBatchCode = models.ForeignKey( - IngredientsRawMaterials, - on_delete=models.CASCADE, - db_column='RawMaterialBatchCode' - ) + IngredientRawMaterialBatchCode = models.ForeignKey(IngredientsRawMaterials, on_delete=models.CASCADE, db_column='RawMaterialBatchCode') RawMaterialName = models.CharField(max_length=255) QuantityUsed = models.IntegerField() - USECATEGORY_CHOICES = [ - ('WBC', 'WBC'), - ('GGB', 'GGB'), - ('Both', 'Both'), - ] + USECATEGORY_CHOICES = [('WBC', 'WBC'), ('GGB', 'GGB'), ('Both', 'Both')] UseCategory = models.CharField(max_length=10, choices=USECATEGORY_CHOICES) DateUsed = models.DateField() + change_status = models.CharField(max_length=15, choices=CHANGE_STATUS_CHOICES, default="active") def save(self, *args, **kwargs): if not self.UsedIngredientBatchCode: @@ -170,20 +152,13 @@ def __str__(self): # -------------------- UsedPackaging -------------------- class UsedPackaging(models.Model): USEDPackagingBatchCode = models.CharField(max_length=50, primary_key=True, blank=True) - PackagingRawMaterialBatchCode = models.ForeignKey( - PackagingRawMaterials, - on_delete=models.CASCADE, - db_column='PackagingBatchCode' - ) + PackagingRawMaterialBatchCode = models.ForeignKey(PackagingRawMaterials, on_delete=models.CASCADE, db_column='PackagingBatchCode') RawMaterialName = models.CharField(max_length=255) QuantityUsed = models.IntegerField() - USECATEGORY_CHOICES = [ - ('WBC', 'WBC'), - ('GGB', 'GGB'), - ('Both', 'Both'), - ] + USECATEGORY_CHOICES = [('WBC', 'WBC'), ('GGB', 'GGB'), ('Both', 'Both')] UseCategory = models.CharField(max_length=10, choices=USECATEGORY_CHOICES) DateUsed = models.DateField() + change_status = models.CharField(max_length=15, choices=CHANGE_STATUS_CHOICES, default="active") def save(self, *args, **kwargs): if not self.USEDPackagingBatchCode: diff --git a/IMAPS/IMAPS/IMAPS_app/static/IMAPS_app/css/styles.css b/IMAPS/IMAPS/IMAPS_app/static/IMAPS_app/css/styles.css index 91ec6f76..334dd54e 100644 --- a/IMAPS/IMAPS/IMAPS_app/static/IMAPS_app/css/styles.css +++ b/IMAPS/IMAPS/IMAPS_app/static/IMAPS_app/css/styles.css @@ -154,6 +154,7 @@ button:hover { /* Table Styles */ .table-container { margin-top: 20px; + overflow: visible !important; } table { @@ -163,7 +164,7 @@ table { margin-top: 20px; background: #fff; border-radius: 20px; - overflow: hidden; + overflow: visible; table-layout: fixed; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); } @@ -823,4 +824,100 @@ td a:hover { max-height: calc(100vh - 150px); /* leave room for header/footer */ overflow-y: auto; /* scroll if it gets too tall */ } - \ No newline at end of file + + .multi-cell { position: relative; } +.toggle-dropdown { cursor: pointer; margin-left: 4px; } +.dropdown-list { + display: none; + position: absolute; + top: 100%; left: 0; + background: #fff; + border: 1px solid #ccc; + list-style: none; margin: 4px 0; padding: 0; + z-index: 10; +} +.dropdown-list li { padding: 4px 8px; } +.multi-cell:hover .dropdown-list, +.multi-cell:focus-within .dropdown-list { display: block; } + + +.multi-cell { + position: relative; /* establish a containing block */ + overflow: visible !important; /* let children overflow this cell */ +} + +.dropdown-list { + display: none; + position: absolute; + top: 70%; /* just under the cell’s bottom edge */ + left: 0; + background: #fff; + border: 1px solid #ccc; + list-style: none; + margin: 4px 0 0; + padding: 0; + z-index: 9999; /* higher than any surrounding container */ + box-shadow: 0 2px 6px rgba(0,0,0,0.2); +} + +.dropdown-list li { + padding: 4px 8px; + white-space: nowrap; +} + + +/* Show on hover */ +.multi-cell:hover .dropdown-list { + display: block; +} + +/* Show when the cell has the “open” class (toggled by JS) */ +.multi-cell.open .dropdown-list { + display: block; +} + +/* Make every input in the update form span the full content width */ +#updateSupplierForm input, +#updateSupplierForm select { + display: block; /* ensure it’s a block-level box */ + width: 100% !important; /* stretch to the container’s full width */ + box-sizing: border-box; /* include padding/border in that width */ +} + +#updateSupplierForm .fields-wrap .field-row:not(:last-child) { + margin-bottom: 0.5rem; +} + + +.dropdown-wrapper { + display: block; + text-align: center; + position: relative; + margin-top: 0.5em; +} +.dropdown-wrapper .toggle-dropdown { + cursor: pointer; + display: inline-block; +} +.dropdown-wrapper .dropdown-list { + display: none; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + background: #fff; + border: 1px solid #ccc; + list-style: none; + margin: 0.25em 0 0; + padding: 0; + z-index: 9999; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); +} +.dropdown-wrapper:hover .dropdown-list, +.dropdown-wrapper.open .dropdown-list { + display: block; +} +.dropdown-wrapper .dropdown-list li { + padding: 0.5em 1em; + white-space: nowrap; +} diff --git a/IMAPS/IMAPS/IMAPS_app/templates/packaging.html b/IMAPS/IMAPS/IMAPS_app/templates/packaging.html index 3fdb3835..071d315a 100644 --- a/IMAPS/IMAPS/IMAPS_app/templates/packaging.html +++ b/IMAPS/IMAPS/IMAPS_app/templates/packaging.html @@ -503,8 +503,8 @@

Delete Used Packaging Entry

{% csrf_token %}
- - + +
@@ -618,6 +618,8 @@

Delete Used Packaging Entry

} let pk = packagingRow.getAttribute("data-packaging-pk"); let formattedDateDelivered = formatDateToISO(dateDelivered); + let form = document.getElementById("updatePackagingForm"); + form.action = `/packaging/update/${pk}/`; document.getElementById("modalPackagingBatchCode").textContent = packagingBatchCode; document.getElementById("modalPackagingBatchCodeHidden").value = packagingBatchCode; document.getElementById('RawMaterialName').value = rawMaterialName; diff --git a/IMAPS/IMAPS/IMAPS_app/templates/suppliers.html b/IMAPS/IMAPS/IMAPS_app/templates/suppliers.html index ee16cbd9..f8ddc0d4 100644 --- a/IMAPS/IMAPS/IMAPS_app/templates/suppliers.html +++ b/IMAPS/IMAPS/IMAPS_app/templates/suppliers.html @@ -98,10 +98,10 @@

Suppliers Inventory

Supplier Code Supplier Name Category + Point Person Contact Number Email Address Social Media - Point Person Options @@ -111,10 +111,53 @@

Suppliers Inventory

{{ sup.SupplierCode }} {{ sup.SupplierName }} {{ sup.Category }} - {{ sup.ContactNumber }} - {{ sup.EmailAddress }} - {{ sup.SocialMedia }} {{ sup.PointPerson }} + {# Contact Number #} + + {{ sup.contact_list.0 }} + {% if sup.contact_list|length > 1 %} + + {% endif %} + + + {# Email Address #} + + {{ sup.email_list.0 }} + {% if sup.email_list|length > 1 %} + + {% endif %} + + + {# Social Media #} + + {{ sup.sm_list.0 }} + {% if sup.sm_list|length > 1 %} + + {% endif %} + + +
- - {% csrf_token %} - - - - - - - - - - - - - - - - - - - - - - -
- - + + +
+ +
+
+ +
-
- - + +
+ + +
+ +
+
+ +
- -
+ +
+ + + + + +
+ + +
+
+ + +
+
+ @@ -359,6 +434,84 @@

Delete Supplier

document.getElementById('supplierFilterMenu').addEventListener('click', () => { alert('Filter options coming soon...'); }); + + // Run once on page load +document.addEventListener('DOMContentLoaded', () => { + // Delegate all “Add more” clicks + document.querySelectorAll('.add-btn').forEach(btn => { + btn.addEventListener('click', () => { + const field = btn.dataset.fieldName; // e.g. "EmailAddress" + const container = btn.closest('.multi-field').querySelector('.fields-wrap'); + // Clone the first row: + const prototype = container.querySelector('.field-row'); + const clone = prototype.cloneNode(true); + const input = clone.querySelector('input'); + input.value = ''; // clear old value + // Re-id it so each is unique (optional): + const idx = container.querySelectorAll('.field-row').length; + input.id = `${input.name.replace('[]','')}_${idx}`; + container.appendChild(clone); + }); + }); +}); + +// Override to inject existing data & open the modal +function openUpdateModalSupplier(pk, supplierCode, supplierName, category, socialMedia, emailAddress, contactNumber, pointPerson) { + const form = document.getElementById('updateSupplierForm'); + form.action = `/suppliers/update/${pk}/`; + + document.getElementById('supplier_code_update').value = supplierCode; + document.getElementById('supplier_name_update').value = supplierName; + document.getElementById('category_update').value = category; + document.getElementById('point_person_update').value = pointPerson; // <— add this line! + + + // Helper: split on comma or semicolon, trim, drop empties + const splitList = str => str + ? str.split(/[;,]/).map(s=>s.trim()).filter(Boolean) + : []; + + // For each multi-field type, clear old rows and re-create + const types = ['SocialMedia','EmailAddress','ContactNumber']; + types.forEach(type => { + const raw = { SocialMedia: socialMedia, + EmailAddress: emailAddress, + ContactNumber: contactNumber, + }[type] || ''; + const list = splitList(raw); + + const wrapper = document.querySelector(`.multi-field[data-field-name="${type}"] .fields-wrap`); + // remove all but the first row: + wrapper.querySelectorAll('.field-row') + .forEach((row, i) => { if (i>0) row.remove(); }); + + // fill + wrapper.querySelector('input').value = list[0]||''; + // clone extras + list.slice(1).forEach(val => { + const clone = wrapper.querySelector('.field-row').cloneNode(true); + clone.querySelector('input').value = val; + wrapper.appendChild(clone); + }); + }); + + openModal('updateSupplierModal'); +} +// Pin‐open on click +document.querySelectorAll('.toggle-dropdown').forEach(icon => { + icon.addEventListener('click', e => { + const wrapper = icon.closest('.dropdown-wrapper'); + wrapper.classList.toggle('open'); + e.stopPropagation(); + }); +}); +// Close when clicking outside +document.addEventListener('click', () => { + document.querySelectorAll('.dropdown-wrapper.open') + .forEach(w => w.classList.remove('open')); +}); + + diff --git a/IMAPS/IMAPS/IMAPS_app/views.py b/IMAPS/IMAPS/IMAPS_app/views.py index 6d916df6..faf2152a 100644 --- a/IMAPS/IMAPS/IMAPS_app/views.py +++ b/IMAPS/IMAPS/IMAPS_app/views.py @@ -17,406 +17,332 @@ PackagingRawMaterialsForm, UsedIngredientForm, UsedPackagingForm, - # New update forms that include fields that were previously excluded: IngredientsRawMaterialsUpdateForm, PackagingRawMaterialsUpdateForm ) PASSWORD = "test123" -##################### -# SUPPLIERS # -##################### +# ================= SUPPLIERS ================= + +import re +from django.shortcuts import render +from .models import Supplier +from .forms import SupplierForm + + def suppliers_list(request): - suppliers = Supplier.objects.all() + # Fetch only active and newly modified suppliers + suppliers = Supplier.objects.filter(change_status__in=["active", "new_modified"]) + + # Prepare a splitter to handle both commas and semicolons + splitter = re.compile(r"[;,]") + for sup in suppliers: + # Convert each delimited string into a clean list + sup.sm_list = [s.strip() for s in splitter.split(sup.SocialMedia or '') if s.strip()] + sup.email_list = [s.strip() for s in splitter.split(sup.EmailAddress or '') if s.strip()] + sup.contact_list = [s.strip() for s in splitter.split(sup.ContactNumber or '') if s.strip()] + sup.pp_list = [s.strip() for s in splitter.split(sup.PointPerson or '') if s.strip()] + create_form = SupplierForm() return render(request, 'suppliers.html', { 'suppliers': suppliers, 'create_form': create_form, }) + + def supplier_create(request): if request.method == 'POST': form = SupplierForm(request.POST) if form.is_valid(): - form.save() + # Don’t save immediately—grab the instance so we can massage the multi-fields + supplier = form.save(commit=False) + + splitter = re.compile(r'[;,]') + + # Re-join each of your array-style inputs back into a single string + for field in ('SocialMedia', 'EmailAddress', 'ContactNumber'): + raw_list = request.POST.getlist(f'{field}[]') + clean = [s.strip() for s in raw_list if s.strip()] + setattr(supplier, field, '; '.join(clean)) + + # PointPerson is a single field on your form, so just pull from cleaned_data + supplier.PointPerson = form.cleaned_data.get('PointPerson', '').strip() + + # Now persist + supplier.save() else: - messages.error(request, "Supplier creation error: " + - "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])) + messages.error( + request, + "Supplier creation error: " + + "; ".join(f"{fld}: {', '.join(errs)}" + for fld, errs in form.errors.items()) + ) return redirect('suppliers_list') +from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect +import re + def supplier_update(request, pk): - supplier = get_object_or_404(Supplier, pk=pk) + old = get_object_or_404(Supplier, pk=pk) if request.method == 'POST': if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - form = SupplierForm(request.POST, instance=supplier) + + # 1) Bind to the existing instance so we UPDATE rather than create + form = SupplierForm(request.POST, instance=old) + if form.is_valid(): - form.save() + # 2) Pull out the clean model instance, don't save yet + supplier = form.save(commit=False) + + # 3) Re-join your multi-input fields + splitter = re.compile(r'[;,]') + for field in ('SocialMedia','EmailAddress','ContactNumber'): + raw_list = request.POST.getlist(f'{field}[]') + clean = [s.strip() for s in raw_list if s.strip()] + setattr(supplier, field, '; '.join(clean)) + + supplier.PointPerson = request.POST.get('PointPerson', '').strip() + + + # 4) Mark it modified and persist + supplier.change_status = 'new_modified' + supplier.save() + else: - messages.error(request, "Supplier update error: " + - "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])) + # gather and show the form errors + msg = "Supplier update error: " + "; ".join( + f"{fld}: {', '.join(errs)}" + for fld, errs in form.errors.items() + ) + messages.error(request, msg) + return redirect('suppliers_list') + def supplier_delete(request, pk): - supplier = get_object_or_404(Supplier, pk=pk) + obj = get_object_or_404(Supplier, pk=pk) if request.method == 'POST': if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - supplier.delete() + obj.change_status = "deleted" + obj.save() return redirect('suppliers_list') -########################## -# INGREDIENTS RAW MATERIALS # -########################## + +# =============== INGREDIENTS =============== + def ingredients_list(request): - ingredients = IngredientsRawMaterials.objects.all() - used_ings = UsedIngredient.objects.all() - create_form = IngredientsRawMaterialsForm() - used_ing_create_form = UsedIngredientForm() - context = { + ingredients = IngredientsRawMaterials.objects.filter(change_status__in=["active", "new_modified"]) + used_ings = UsedIngredient.objects.filter(change_status__in=["active", "new_modified"]) + return render(request, 'ingredients.html', { 'ingredients': ingredients, 'used_ings': used_ings, - 'create_form': create_form, - 'used_ing_create_form': used_ing_create_form, - } - return render(request, 'ingredients.html', context) - + 'create_form': IngredientsRawMaterialsForm(), + 'used_ing_create_form': UsedIngredientForm(), + }) def ingredients_create(request): if request.method == 'POST': form = IngredientsRawMaterialsForm(request.POST) if form.is_valid(): - existing_batch = form.cleaned_data.get('existing_batch') - quantity_bought = form.cleaned_data.get('QuantityBought') - if existing_batch and existing_batch != 'None': - try: - # Get the existing record (assumed to be unique by batch code) - existing_record = IngredientsRawMaterials.objects.get(RawMaterialBatchCode=existing_batch) - - # Create a new record but don’t save yet: - new_record = form.save(commit=False) - # The new record’s QuantityLeft should be just its own QuantityBought - # before combining; then we compute the new total: - new_record.QuantityLeft = new_record.QuantityBought - new_record.save() - - # Now compute the combined quantity left: - combined = existing_record.QuantityLeft + new_record.QuantityLeft - # Update both records to show the same combined total. - existing_record.QuantityLeft = combined - existing_record.save() - - new_record.QuantityLeft = combined - new_record.save() - except IngredientsRawMaterials.DoesNotExist: - form.save() - else: - form.save() + form.save() else: - messages.error(request, "Ingredient creation error: " + - "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])) - pass + messages.error(request, + "Ingredient creation error: " + + "; ".join(f"{field}: {', '.join(errs)}" for field, errs in form.errors.items()) + ) return redirect('ingredients_list') def ingredients_update(request, pk): - ingredient = get_object_or_404(IngredientsRawMaterials, pk=pk) + old = get_object_or_404(IngredientsRawMaterials, pk=pk) if request.method == 'POST': if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - form = IngredientsRawMaterialsUpdateForm(request.POST, instance=ingredient) + form = IngredientsRawMaterialsUpdateForm(request.POST) if form.is_valid(): - updated = form.save() # persists DateDelivered, ExpirationDate, etc. - # recompute QuantityLeft across all batches - batches = IngredientsRawMaterials.objects.filter( - RawMaterialName=updated.RawMaterialName + old.change_status = "old_modified" + old.save() + new = form.save(commit=False) + new.change_status = "new_modified" + new.save() + else: + messages.error(request, + "Ingredient update error: " + + "; ".join(f"{field}: {', '.join(errs)}" for field, errs in form.errors.items()) ) - total_bought = batches.aggregate(total=Sum('QuantityBought'))['total'] or 0 - total_used = UsedIngredient.objects.filter( - RawMaterialName=updated.RawMaterialName - ).aggregate(total=Sum('QuantityUsed'))['total'] or 0 - new_left = max(total_bought - total_used, 0) - batches.update(QuantityLeft=new_left) - return redirect('ingredients_list') - else: - # GET → prepopulate form (including date widget) - form = IngredientsRawMaterialsUpdateForm(instance=ingredient) - - return render(request, 'ingredients_update.html', { - 'form': form, - 'ingredient': ingredient - }) + return redirect('ingredients_list') def ingredients_delete(request, pk): - ingredient = get_object_or_404(IngredientsRawMaterials, pk=pk) - - if request.method == "POST": - if request.POST.get("password") != PASSWORD: + obj = get_object_or_404(IngredientsRawMaterials, pk=pk) + if request.method == 'POST': + if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - - raw_name = ingredient.RawMaterialName # ① remember the family - ingredient.delete() # ② remove the row - - # ③–④ recalc QuantityLeft across the survivors - batches = IngredientsRawMaterials.objects.filter(RawMaterialName=raw_name) - if batches.exists(): - total_bought = batches.aggregate(t=Sum("QuantityBought"))["t"] or 0 - total_used = UsedIngredient.objects.filter( - RawMaterialName=raw_name - ).aggregate(t=Sum("QuantityUsed"))["t"] or 0 - new_left = max(total_bought - total_used, 0) - batches.update(QuantityLeft=new_left) - - return redirect("ingredients_list") + obj.change_status = "deleted" + obj.save() + return redirect('ingredients_list') +# ============= USED INGREDIENTS ============= def used_ingredients_create(request): if request.method == 'POST': form = UsedIngredientForm(request.POST) if form.is_valid(): - used_ing = form.save() - # Get all ingredient records for the same raw material name. - qs = IngredientsRawMaterials.objects.filter(RawMaterialName=used_ing.RawMaterialName) - total_left = sum(record.QuantityLeft for record in qs) - new_total = total_left - used_ing.QuantityUsed - if new_total < 0: - new_total = 0 - # Update all records with this new total so they show the same remaining balance. - for record in qs: - record.QuantityLeft = new_total - record.save() + used = form.save(commit=False) + used.RawMaterialName = used.IngredientRawMaterialBatchCode.RawMaterialName + used.save() + batches = IngredientsRawMaterials.objects.filter(RawMaterialName=used.RawMaterialName) + new_bal = max(batches.first().QuantityLeft - used.QuantityUsed, 0) if batches.exists() else 0 + batches.update(QuantityLeft=new_bal) else: - messages.error(request, "Used ingredient creation error: " + - "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])) - pass - return redirect('ingredients_list') - -def used_ingredients_create(request): - if request.method == "POST": - form = UsedIngredientForm(request.POST) - if form.is_valid(): - # 1️⃣ Save later so we can fill-in RawMaterialName automatically - used_ing = form.save(commit=False) - used_ing.RawMaterialName = ( - used_ing.IngredientRawMaterialBatchCode.RawMaterialName - ) - used_ing.save() # now the UsedIngredient row exists - - # 2️⃣ Recompute the shared running balance for this ingredient - batches = IngredientsRawMaterials.objects.filter( - RawMaterialName=used_ing.RawMaterialName - ) - current_balance = ( - batches.first().QuantityLeft if batches.exists() else 0 - ) - new_balance = max(current_balance - used_ing.QuantityUsed, 0) - - # 3️⃣ Propagate the new balance to every sibling batch in one query - batches.update(QuantityLeft=new_balance) - else: - messages.error( - request, - "Used ingredient creation error: " - + "; ".join( - f"{field}: {', '.join(errs)}" - for field, errs in form.errors.items() - ), + messages.error(request, + "Used ingredient creation error: " + + "; ".join(f"{field}: {', '.join(errs)}" for field, errs in form.errors.items()) ) - return redirect("ingredients_list") + return redirect('ingredients_list') def used_ingredients_update(request, pk): - used_ing = get_object_or_404(UsedIngredient, pk=pk) + old = get_object_or_404(UsedIngredient, pk=pk) if request.method == 'POST': if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - form = UsedIngredientForm(request.POST, instance=used_ing) + form = UsedIngredientForm(request.POST) if form.is_valid(): - form.save() + old.change_status = "old_modified" + old.save() + new = form.save(commit=False) + new.RawMaterialName = new.IngredientRawMaterialBatchCode.RawMaterialName + new.change_status = "new_modified" + new.save() else: - messages.error(request, "Used ingredient update error: " + - "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])) + messages.error(request, + "Used ingredient update error: " + + "; ".join(f"{field}: {', '.join(errs)}" for field, errs in form.errors.items()) + ) return redirect('ingredients_list') def used_ingredients_delete(request, pk): - used_ing = get_object_or_404(UsedIngredient, pk=pk) + obj = get_object_or_404(UsedIngredient, pk=pk) if request.method == 'POST': if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - ingredient = used_ing.IngredientRawMaterialBatchCode - ingredient.QuantityLeft += used_ing.QuantityUsed - ingredient.save() - used_ing.delete() + obj.change_status = "deleted" + obj.save() return redirect('ingredients_list') -############################## -# PACKAGING RAW MATERIALS # -############################## -def packaging_list(request): - materials = PackagingRawMaterials.objects.all() - used_packs = UsedPackaging.objects.all() - create_form = PackagingRawMaterialsForm() - used_pack_create_form = UsedPackagingForm() - context = { - 'materials': materials, - 'used_packs': used_packs, - 'create_form': create_form, - 'used_pack_create_form': used_pack_create_form, - } - return render(request, 'packaging.html', context) +# =============== PACKAGING =============== +def packaging_list(request): + mats = PackagingRawMaterials.objects.filter(change_status__in=["active", "new_modified"]) + used = UsedPackaging.objects.filter(change_status__in=["active", "new_modified"]) + return render(request, 'packaging.html', { + 'materials': mats, + 'used_packs': used, + 'create_form': PackagingRawMaterialsForm(), + 'used_pack_create_form': UsedPackagingForm(), + }) def packaging_create(request): if request.method == 'POST': form = PackagingRawMaterialsForm(request.POST) if form.is_valid(): - existing_batch = form.cleaned_data.get('existing_batch') - quantity_bought = form.cleaned_data.get('QuantityBought') - if existing_batch and existing_batch != 'None': - try: - # Get the existing packaging record by its batch code. - existing_record = PackagingRawMaterials.objects.get(PackagingBatchCode=existing_batch) - - # Create a new record from the form but do not save yet. - new_record = form.save(commit=False) - - # Override the following fields with the values from the existing record. - new_record.ContainerSize = existing_record.ContainerSize - new_record.RawMaterialName = existing_record.RawMaterialName - new_record.UseCategory = existing_record.UseCategory - # Ensure the new record's QuantityLeft starts as its own QuantityBought. - new_record.QuantityLeft = new_record.QuantityBought - new_record.save() - - # Calculate the combined available quantity. - combined_quantity = existing_record.QuantityLeft + new_record.QuantityLeft - # Update both records to share the new combined quantity. - existing_record.QuantityLeft = combined_quantity - existing_record.save() - - new_record.QuantityLeft = combined_quantity - new_record.save() - except PackagingRawMaterials.DoesNotExist: - form.save() - else: - form.save() + form.save() else: - messages.error(request, "Packaging creation error: " + - "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])) - pass + messages.error(request, + "Packaging creation error: " + + "; ".join(f"{field}: {', '.join(errs)}" for field, errs in form.errors.items()) + ) return redirect('packaging_list') def packaging_update(request, pk): - material = get_object_or_404(PackagingRawMaterials, pk=pk) + old = get_object_or_404(PackagingRawMaterials, pk=pk) if request.method == 'POST': if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - form = PackagingRawMaterialsUpdateForm(request.POST, instance=material) + form = PackagingRawMaterialsUpdateForm(request.POST) if form.is_valid(): - updated = form.save() # persists DateDelivered, ContainerSize, etc. - # recompute QuantityLeft across all packaging batches - batches = PackagingRawMaterials.objects.filter( - RawMaterialName=updated.RawMaterialName + old.change_status = "old_modified" + old.save() + new = form.save(commit=False) + new.change_status = "new_modified" + new.save() + batches = PackagingRawMaterials.objects.filter(RawMaterialName=new.RawMaterialName) + bought = batches.aggregate(total=Sum('QuantityBought'))['total'] or 0 + used_sum = UsedPackaging.objects.filter(RawMaterialName=new.RawMaterialName).aggregate(total=Sum('QuantityUsed'))['total'] or 0 + batches.update(QuantityLeft=max(bought-used_sum,0)) + else: + messages.error(request, + "Packaging update error: " + + "; ".join(f"{field}: {', '.join(errs)}" for field, errs in form.errors.items()) ) - total_bought = batches.aggregate(total=Sum('QuantityBought'))['total'] or 0 - total_used = UsedPackaging.objects.filter( - RawMaterialName=updated.RawMaterialName - ).aggregate(total=Sum('QuantityUsed'))['total'] or 0 - new_left = max(total_bought - total_used, 0) - batches.update(QuantityLeft=new_left) - return redirect('packaging_list') - else: - # GET → prepopulate form - form = PackagingRawMaterialsUpdateForm(instance=material) - - return render(request, 'packaging_update.html', { - 'form': form, - 'material': material - }) + return redirect('packaging_list') def packaging_delete(request, pk): - packaging = get_object_or_404(PackagingRawMaterials, pk=pk) - - if request.method == "POST": - if request.POST.get("password") != PASSWORD: + obj = get_object_or_404(PackagingRawMaterials, pk=pk) + if request.method == 'POST': + if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) + obj.change_status = "deleted" + obj.save() + return redirect('packaging_list') - raw_name = packaging.RawMaterialName # ① remember the family - packaging.delete() # ② remove the row - - # ③ – ④ recalc QuantityLeft across remaining batches - batches = PackagingRawMaterials.objects.filter(RawMaterialName=raw_name) - if batches.exists(): - total_bought = batches.aggregate(t=Sum("QuantityBought"))["t"] or 0 - total_used = UsedPackaging.objects.filter( - RawMaterialName=raw_name - ).aggregate(t=Sum("QuantityUsed"))["t"] or 0 - new_left = max(total_bought - total_used, 0) - batches.update(QuantityLeft=new_left) - - return redirect("packaging_list") +# ============ USED PACKAGING ============ def used_packaging_create(request): if request.method == 'POST': form = UsedPackagingForm(request.POST) if form.is_valid(): - # --- 1. Save but let us tweak fields first ----------------------- - used_pack = form.save(commit=False) - # Auto-fill the name from the selected batch - used_pack.RawMaterialName = ( - used_pack.PackagingRawMaterialBatchCode.RawMaterialName - ) - used_pack.save() # now the row is persisted - - # --- 2. Recompute the running balance --------------------------- - # Every batch for this packaging material mirrors the *same* balance, - # so read QuantityLeft from any one of them (first()). - batches = PackagingRawMaterials.objects.filter( - RawMaterialName=used_pack.RawMaterialName - ) - current_balance = batches.first().QuantityLeft if batches.exists() else 0 - new_balance = max(current_balance - used_pack.QuantityUsed, 0) - - # Update all sibling batches to reflect the new balance in one go - batches.update(QuantityLeft=new_balance) + used = form.save(commit=False) + used.RawMaterialName = used.PackagingRawMaterialBatchCode.RawMaterialName + used.save() + batches = PackagingRawMaterials.objects.filter(RawMaterialName=used.RawMaterialName) + new_bal = max(batches.first().QuantityLeft - used.QuantityUsed, 0) if batches.exists() else 0 + batches.update(QuantityLeft=new_bal) else: - messages.error( - request, + messages.error(request, "Used packaging creation error: " + - "; ".join(f"{field}: {', '.join(errs)}" - for field, errs in form.errors.items()) + "; ".join(f"{field}: {', '.join(errs)}" for field, errs in form.errors.items()) ) return redirect('packaging_list') def used_packaging_update(request, pk): - used_pack = get_object_or_404(UsedPackaging, pk=pk) + old = get_object_or_404(UsedPackaging, pk=pk) if request.method == 'POST': if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - form = UsedPackagingForm(request.POST, instance=used_pack) + form = UsedPackagingForm(request.POST) if form.is_valid(): - form.save() + old.change_status = "old_modified" + old.save() + new = form.save(commit=False) + new.RawMaterialName = new.PackagingRawMaterialBatchCode.RawMaterialName + new.change_status = "new_modified" + new.save() else: - messages.error(request, "Used packaging update error: " + - "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])) + messages.error(request, + "Used packaging update error: " + + "; ".join(f"{field}: {', '.join(errs)}" for field, errs in form.errors.items()) + ) return redirect('packaging_list') def used_packaging_delete(request, pk): - used_pack = get_object_or_404(UsedPackaging, pk=pk) + obj = get_object_or_404(UsedPackaging, pk=pk) if request.method == 'POST': if request.POST.get('password') != PASSWORD: return HttpResponse("Incorrect password", status=403) - packaging = used_pack.PackagingRawMaterialBatchCode - packaging.QuantityLeft += used_pack.QuantityUsed - packaging.save() - used_pack.delete() + obj.change_status = "deleted" + obj.save() return redirect('packaging_list') -########################## -# REPORT SUMMARY # -########################## + +# ============ REPORT SUMMARY ============ + def report_summary(request): start_date = request.GET.get('start_date') end_date = request.GET.get('end_date') @@ -425,41 +351,27 @@ def report_summary(request): exp_ing = [] if start_date and end_date: - # total used - used_ing = (UsedIngredient.objects - .filter(DateUsed__range=[start_date, end_date]) - .values('RawMaterialName') - .annotate(total_used=Sum('QuantityUsed'))) - used_pack = (UsedPackaging.objects - .filter(DateUsed__range=[start_date, end_date]) - .values('RawMaterialName') - .annotate(total_used=Sum('QuantityUsed'))) - - # calculate expired and remaining - # sum up quantity left at the moment of expiry - expired_qs = (IngredientsRawMaterials.objects - .filter(ExpirationDate__range=[start_date, end_date]) - .values('RawMaterialName') - .annotate(expired_qty=Sum('QuantityLeft'))) - - # sum up quantity left outside that expiry window - remaining_qs = (IngredientsRawMaterials.objects - .exclude(ExpirationDate__range=[start_date, end_date]) - .values('RawMaterialName') - .annotate(remaining_qty=Sum('QuantityLeft'))) - - # merge the two querysets into exp_ing list of dicts - exp_ing = [] - # build a map for remaining + used_ing = UsedIngredient.objects.filter( + DateUsed__range=[start_date, end_date], + change_status__in=["active","new_modified"] + ).values('RawMaterialName').annotate(total_used=Sum('QuantityUsed')) + used_pack = UsedPackaging.objects.filter( + DateUsed__range=[start_date, end_date], + change_status__in=["active","new_modified"] + ).values('RawMaterialName').annotate(total_used=Sum('QuantityUsed')) + expired_qs = IngredientsRawMaterials.objects.filter( + ExpirationDate__range=[start_date, end_date], + change_status__in=["active","new_modified"] + ).values('RawMaterialName').annotate(expired_qty=Sum('QuantityLeft')) + remaining_qs = IngredientsRawMaterials.objects.exclude( + ExpirationDate__range=[start_date, end_date] + ).filter(change_status__in=["active","new_modified"]).values('RawMaterialName').annotate(remaining_qty=Sum('QuantityLeft')) rem_map = {r['RawMaterialName']: r['remaining_qty'] for r in remaining_qs} for e in expired_qs: - name = e['RawMaterialName'] - exp_qty = e['expired_qty'] - rem_qty = rem_map.get(name, 0) exp_ing.append({ - 'RawMaterialName': name, - 'expired_qty': exp_qty, - 'remaining_qty': rem_qty + 'RawMaterialName': e['RawMaterialName'], + 'expired_qty': e['expired_qty'], + 'remaining_qty': rem_map.get(e['RawMaterialName'], 0) }) return render(request, 'report_summary.html', { 'used_ing': used_ing, @@ -469,14 +381,18 @@ def report_summary(request): 'end_date': end_date, }) + def supplier_list_packaging(request): suppliers = Supplier.objects.filter( - Category__in=['Packaging', 'Both'] - ).values('SupplierCode', 'SupplierName') + change_status__in=["active","new_modified"], + Category__in=['Packaging','Both'] + ).values('SupplierCode','SupplierName') return JsonResponse({'suppliers': list(suppliers)}, safe=False) + def supplier_list_ingredients(request): suppliers = Supplier.objects.filter( - Category__in=['Ingredient', 'Both'] - ).values('SupplierCode', 'SupplierName') + change_status__in=["active","new_modified"], + Category__in=['Ingredient','Both'] + ).values('SupplierCode','SupplierName') return JsonResponse({'suppliers': list(suppliers)}, safe=False) diff --git a/IMAPS/requirements.txt b/IMAPS/requirements.txt new file mode 100644 index 00000000..00eface2 Binary files /dev/null and b/IMAPS/requirements.txt differ