diff --git a/.gitignore b/.gitignore index 21b11eb4e4a540f1150e1bd20110e13299dba0d9..9e6ac02ad951acc488a7336b7b60873410db94e5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,10 @@ __pycache__/ .python-version .coverage .env_var -static \ No newline at end of file +static +env +.env +.scannerwork +.vscode +coverage.xml +media \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e723c48a27c6603bfdad04bd9c61d1176731d050 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.8-slim-buster +ENV PYTHONUNBUFFERED=1 +WORKDIR /code +COPY requirements.txt /code/ +RUN pip3 install -r requirements.txt +COPY . /code/ +COPY wait-for-it.sh /wait-for-it.sh +RUN chmod +x /wait-for-it.sh \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 25dd94b55994c8f8b19002bbdbacc2e5ea5870ba..715d6067caaa544a0000d35a687999c0c54dc6cc --- a/README.md +++ b/README.md @@ -1,126 +1,135 @@ -# Home Industry API - -[![pipeline status](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-backend/badges/master/pipeline.svg)](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-backend/commits/master) -[![coverage report](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-backend/badges/master/coverage.svg)](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/2020/ppl-c/diskominfo-depok-tpu-online/post-rpl-backend/commits/master) - -## Table of Contents - -- [Environment Variables](#environment-variables) - - [Local](#local) - - [CI](#ci) - - [Staging](#staging) - - [Production](#production) -- [Local Configuration](#local-configuration) -- [Deployed API URLs](#deployed-api-urls) -- [API Documentation](#api-documentation) - -## Environment Variables - -### Local - -Key | Required | Example ---- | --- | --- -`DATABASE_HOST` | yes | `127.0.0.1` -`DATABASE_NAME` | yes | `home_industry` -`DATABASE_PASSWORD` | yes | `postgres` -`DATABASE_PORT` | yes | `5432` -`DATABASE_USER` | yes | `postgres` -`DEBUG` | no | `True` -`DJANGO_SETTINGS_MODULE` | yes | `home_industry.settings.local` -`SECRET_KEY` | yes | `7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u` - -### CI - -Key | Required | Example ---- | --- | --- -`DJANGO_SETTINGS_MODULE` | yes | `home_industry.settings.ci` -`SECRET_KEY` | yes | `7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u` - -### Staging - -Key | Required | Example ---- | --- | --- -`AWS_ACCESS_KEY_ID` | yes | `AKIAIOSFODNN7EXAMPLE` -`AWS_REGION_NAME` | yes | `ap-southeast-1` -`AWS_SECRET_ACCESS_KEY` | yes | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` -`AWS_STORAGE_BUCKET_NAME` | yes | `homeindustry-api` -`DATABASE_URL` | yes | `postgres://postgres:postgres@ec2-117-21-174-214.compute-1.amazonaws.com:5432/home_industry` -`DEBUG` | no | `True` -`DJANGO_SETTINGS_MODULE` | yes | `home_industry.settings.staging` -`HOME_INDUSTRY_ADMIN_SITE_URL` | no | `https://homeindustry.com/admin/` -`HOME_INDUSTRY_ADMIN_SITE_USER_PATH` | no | `users/` -`HOME_INDUSTRY_ADMIN_SITE_PRODUCT_PATH` | no | `products/` -`HOME_INDUSTRY_ADMIN_SITE_TRANSACTION_PATH` | no | `transactions/` -`HOME_INDUSTRY_ADMIN_SITE_PROGRAM_PATH` | no | `programs/` -`HOME_INDUSTRY_ADMIN_SITE_PROGRAM_DONATION_PATH` | no | `program-donations/` -`SECRET_KEY` | yes | `7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u` - -### Production - -Key | Required | Example ---- | --- | --- -`AWS_ACCESS_KEY_ID` | yes | `AKIAIOSFODNN7EXAMPLE` -`AWS_REGION_NAME` | yes | `ap-southeast-1` -`AWS_SECRET_ACCESS_KEY` | yes | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` -`AWS_STORAGE_BUCKET_NAME` | yes | `homeindustry-api` -`DATABASE_HOST` | yes | `127.0.0.1` -`DATABASE_NAME` | yes | `home_industry` -`DATABASE_PASSWORD` | yes | `postgres` -`DATABASE_PORT` | yes | `5432` -`DATABASE_USER` | yes | `postgres` -`DJANGO_SETTINGS_MODULE` | yes | `home_industry.settings.production` -`HOME_INDUSTRY_ADMIN_SITE_URL` | no | `https://homeindustry.com/admin/` -`HOME_INDUSTRY_ADMIN_SITE_USER_PATH` | no | `users/` -`HOME_INDUSTRY_ADMIN_SITE_PRODUCT_PATH` | no | `products/` -`HOME_INDUSTRY_ADMIN_SITE_TRANSACTION_PATH` | no | `transactions/` -`HOME_INDUSTRY_ADMIN_SITE_PROGRAM_PATH` | no | `programs/` -`HOME_INDUSTRY_ADMIN_SITE_PROGRAM_DONATION_PATH` | no | `program-donations/` -`SECRET_KEY` | yes | `7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u` - -## Local Configuration - -- Install Python 3.6 or higher and PostgreSQL -- Create Python virtual environment - ```bash - $ cd /path/to/project/directory - $ python3 -m venv env - $ source env/bin/activate - $ pip3 install -r requirements.txt - ``` -- Create PostgreSQL database -- Set up environment variables (change as needed) - ```bash - $ export DATABASE_HOST='127.0.0.1' - $ export DATABASE_NAME='home_industry' - $ export DATABASE_PASSWORD='postgres' - $ export DATABASE_PORT='5432' - $ export DATABASE_USER='postgres' - $ export DJANGO_SETTINGS_MODULE='home_industry.settings.local' - $ export SECRET_KEY='7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u' - ``` -- Migrate the database - ```bash - $ python3 manage.py migrate - ``` -- Set up API configuration by modifying `api_config.yaml` -- Create or update the API configuration in database - ```bash - $ python3 manage.py createorupdateapiconfig - ``` -- Create Superuser - ```bash - $ python3 manage.py createsuperuser - ``` -- Run server - ```bash - $ python3 manage.py runserver - ``` - -## Deployed API URLs - -- Staging: [https://industripilar-staging.herokuapp.com](https://industripilar-staging.herokuapp.com) -- Production: [https://api.industripilar.com](https://api.industripilar.com) - -## API Documentation - -[https://industripilar-staging.herokuapp.com/docs/](https://industripilar-staging.herokuapp.com/docs/) +# Home Industry API + +[![pipeline status](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/sosial/pilar/pilar-backend/badges/dev/pipeline.svg)](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/sosial/pilar/pilar-backend/-/commits/dev) +[![coverage report](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/sosial/pilar/pilar-backend/badges/dev/coverage.svg)](https://gitlab.cs.ui.ac.id/ppl-fasilkom-ui/sosial/pilar/pilar-backend/-/commits/dev) +## Table of Contents + +- [Environment Variables](#environment-variables) + - [Local](#local) + - [CI](#ci) + - [Staging](#staging) + - [Production](#production) +- [Local Configuration](#local-configuration) +- [Deployed API URLs](#deployed-api-urls) +- [API Documentation](#api-documentation) + +## Environment Variables + +### Local + +Key | Required | Example +--- | --- | --- +`DATABASE_HOST` | yes | `127.0.0.1` +`DATABASE_NAME` | yes | `home_industry` +`DATABASE_PASSWORD` | yes | `postgres` +`DATABASE_PORT` | yes | `5432` +`DATABASE_USER` | yes | `postgres` +`DEBUG` | no | `True` +`DJANGO_SETTINGS_MODULE` | yes | `home_industry.settings.local` +`SECRET_KEY` | yes | `7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u` + +### CI + +Key | Required | Example +--- | --- | --- +`DJANGO_SETTINGS_MODULE` | yes | `home_industry.settings.ci` +`SECRET_KEY` | yes | `7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u` + +### Staging + +Key | Required | Example +--- | --- | --- +`AWS_ACCESS_KEY_ID` | yes | `AKIAIOSFODNN7EXAMPLE` +`AWS_REGION_NAME` | yes | `ap-southeast-1` +`AWS_SECRET_ACCESS_KEY` | yes | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` +`AWS_STORAGE_BUCKET_NAME` | yes | `homeindustry-api` +`DATABASE_URL` | yes | `postgres://postgres:postgres@ec2-117-21-174-214.compute-1.amazonaws.com:5432/home_industry` +`DEBUG` | no | `True` +`DJANGO_SETTINGS_MODULE` | yes | `home_industry.settings.staging` +`HOME_INDUSTRY_ADMIN_SITE_URL` | no | `https://homeindustry.com/admin/` +`HOME_INDUSTRY_ADMIN_SITE_USER_PATH` | no | `users/` +`HOME_INDUSTRY_ADMIN_SITE_PRODUCT_PATH` | no | `products/` +`HOME_INDUSTRY_ADMIN_SITE_TRANSACTION_PATH` | no | `transactions/` +`HOME_INDUSTRY_ADMIN_SITE_PROGRAM_PATH` | no | `programs/` +`HOME_INDUSTRY_ADMIN_SITE_PROGRAM_DONATION_PATH` | no | `program-donations/` +`SECRET_KEY` | yes | `7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u` + +### Production + +Key | Required | Example +--- | --- | --- +`AWS_ACCESS_KEY_ID` | yes | `AKIAIOSFODNN7EXAMPLE` +`AWS_REGION_NAME` | yes | `ap-southeast-1` +`AWS_SECRET_ACCESS_KEY` | yes | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` +`AWS_STORAGE_BUCKET_NAME` | yes | `homeindustry-api` +`DATABASE_HOST` | yes | `127.0.0.1` +`DATABASE_NAME` | yes | `home_industry` +`DATABASE_PASSWORD` | yes | `postgres` +`DATABASE_PORT` | yes | `5432` +`DATABASE_USER` | yes | `postgres` +`DJANGO_SETTINGS_MODULE` | yes | `home_industry.settings.production` +`HOME_INDUSTRY_ADMIN_SITE_URL` | no | `https://homeindustry.com/admin/` +`HOME_INDUSTRY_ADMIN_SITE_USER_PATH` | no | `users/` +`HOME_INDUSTRY_ADMIN_SITE_PRODUCT_PATH` | no | `products/` +`HOME_INDUSTRY_ADMIN_SITE_TRANSACTION_PATH` | no | `transactions/` +`HOME_INDUSTRY_ADMIN_SITE_PROGRAM_PATH` | no | `programs/` +`HOME_INDUSTRY_ADMIN_SITE_PROGRAM_DONATION_PATH` | no | `program-donations/` +`SECRET_KEY` | yes | `7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u` + +## Local Configuration + +- Create environment variables in root folder (change as needed) + + Filename: `.env` + ```bash + DATABASE_HOST=db + DATABASE_NAME=home_industry + DATABASE_PASSWORD=postgres + DATABASE_PORT=5432 + DATABASE_USER=postgres + DJANGO_SETTINGS_MODULE=home_industry.settings.local + SECRET_KEY=7&s33ax$lxxzti1)0y=8#tu!$7bdy)p$1@kn06tp&8x8i9#h2u + ALLOWED_HOST=0.0.0.0 + ``` +- Change `wait-for-it.sh` permission + ```bash + $ chmod +x wait-for-it.sh + ``` +- Run docker-compose + ```bash + $ sudo docker-compose up + ``` +- In another terminal, check running container + ```bash + $ docker ps + ``` +- Run collectstatic + ```bash + $ docker exec -it python manage.py collectstatic --noinput + ``` +- Create superuser + ```bash + $ docker exec -it python manage.py createsuperuser + ``` +- Create or update API configuration in database + ```bash + $ docker exec -it python manage.py createorupdateapiconfig + ``` +- Generate dummy data from seeders for database + ```bash + $ docker exec -it python manage.py createdummydata + ``` +- Access database + ```bash + $ docker exec -it psql -U postgres -d home_industry -h db + ``` + +## Deployed API URLs + +- Development: [https://pilar-be-dev.cs.ui.ac.id](https://pilar-be-dev.cs.ui.ac.id) +- Staging: [https://pilar-be-staging.cs.ui.ac.id](https://pilar-be-staging.cs.ui.ac.id) +- Production: [https://pilar-be.cs.ui.ac.id](https://pilar-be.cs.ui.ac.id) + +## API Documentation + +[https://pilar-be-dev.cs.ui.ac.id/docs/](https://pilar-be-dev.cs.ui.ac.id/docs/) diff --git a/api/management/commands/createdummydata.py b/api/management/commands/createdummydata.py new file mode 100644 index 0000000000000000000000000000000000000000..86785d76584ecf109ccfb07e8454a5667cc0f2dc --- /dev/null +++ b/api/management/commands/createdummydata.py @@ -0,0 +1,37 @@ +from django.core.management import base +from api import models +from api import seeds + +class Command(base.BaseCommand): + def handle(self, *args, **kwargs): + user, status = models.User.objects.get_or_create(**seeds.USER_DATA) + bank_account_transfer_destination, status = models.BankAccountTransferDestination.objects.get_or_create(**seeds.BANK_ACCOUNT_TRANSFER_DESTINATION) + + category, status = models.Category.objects.get_or_create(**seeds.CATEGORY_DATA) + subcategory, status = models.Subcategory.objects.get_or_create(**dict( + seeds.SUBCATEGORY_DATA, + category=category + )) + product, status = models.Product.objects.get_or_create(**dict(seeds.PRODUCT_DATA, subcategory=subcategory)) + transaction, status = models.Transaction.objects.get_or_create(**dict( + seeds.TRANSACTION_DATA, user=user + )) + transaction_item, status = models.TransactionItem.objects.get_or_create(**dict( + seeds.TRANSACTION_ITEM_DATA, + transaction=transaction, + product=product + )) + + program, status = models.Program.objects.get_or_create(**seeds.PROGRAM_DATA) + program_donation_pck, status = models.ProgramDonation.objects.get_or_create(**dict( + seeds.PROGRAM_DONATION_CASH_DATA, + user=user, + program=program + )) + program_donation_dlv, status = models.ProgramDonation.objects.get_or_create(**dict( + seeds.PROGRAM_DONATION_CASH_DATA, + user=user, + program=program + )) + + batch, status = models.Batch.objects.get_or_create(**seeds.BATCH_DATA) diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 4eaad886774517d3a170ad3e3278f16172d938c3..e7dfbaf08336fbed8ae176032b0e5b3690eb7ebe 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,19 +1,20 @@ -# Generated by Django 3.0.7 on 2020-10-22 09:08 +# Generated by Django 3.0.7 on 2021-06-06 13:02 -import api.utils +import re +import uuid from decimal import Decimal -from django.conf import settings -from django.contrib.postgres import operations as postgres_operations + +import api.utils import django.contrib.auth.models import django.contrib.auth.validators import django.contrib.postgres.fields.citext import django.core.validators -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import phonenumber_field.modelfields -import re -import uuid +from django.conf import settings +from django.contrib.postgres import operations as postgres_operations +from django.db import migrations, models class Migration(migrations.Migration): @@ -75,6 +76,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')), ('bank_name', models.CharField(max_length=100, verbose_name='bank name')), + ('bank_code_number', models.CharField(default='000', max_length=100, verbose_name='bank code number')), ('bank_account_number', models.CharField(max_length=100, verbose_name='bank account number')), ('bank_account_name', models.CharField(max_length=200, verbose_name='bank account name')), ], @@ -133,8 +135,15 @@ class Migration(migrations.Migration): ('description', models.TextField(verbose_name='description')), ('price', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='price')), ('stock', models.PositiveIntegerField(blank=True, null=True, verbose_name='stock')), - ('pre_order', models.BooleanField(default=False, verbose_name='pre-order')), + ('modal', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='modal')), + ('profit', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='profit')), ('image', models.ImageField(blank=True, null=True, upload_to=api.utils.get_upload_file_path, verbose_name='image')), + ('total_profit', models.DecimalField(blank=True, decimal_places=2, default=Decimal('0'), max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='total profit')), + ('is_hampers', models.BooleanField(default=False, verbose_name='the product is hampers package')), + ('hampers_price', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], verbose_name='hampers price')), + ('unit', models.CharField(default='buah', max_length=200, verbose_name='unit')), + ('preorder', models.BooleanField(blank=True, default=False, null=True, verbose_name='preorder')), + ('preorder_duration', models.PositiveIntegerField(blank=True, default=0, null=True, verbose_name='preorder duration')), ], options={ 'verbose_name': 'product', @@ -202,6 +211,7 @@ class Migration(migrations.Migration): ('transfer_destination_bank_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='transfer destination bank name')), ('transfer_destination_bank_account_name', models.CharField(blank=True, max_length=200, null=True, verbose_name='transfer destination bank account name')), ('transfer_destination_bank_account_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='transfer destination bank account number')), + ('transfer_destination_bank_code_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='transfer destination bank code number')), ('created_at', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created at')), ('updated_at', models.DateTimeField(auto_now=True, db_index=True, verbose_name='updated at')), ('batch_name', models.CharField(max_length=200, verbose_name='batch name')), @@ -222,10 +232,12 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')), ('product_name', models.CharField(max_length=200, verbose_name='product name')), ('product_price', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='product price')), - ('product_pre_order', models.BooleanField(verbose_name='product pre-order')), + ('hampers_price', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.00'))], verbose_name='hampers price')), + ('hampers_messages', models.CharField(default='', max_length=100, verbose_name='hampers messages')), ('quantity', models.PositiveIntegerField(verbose_name='quantity')), ('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transaction_items', to='api.Product', verbose_name='product')), ('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transaction_items', to='api.Transaction', verbose_name='transaction')), + ('profit', models.DecimalField(blank=True, decimal_places=2, default=Decimal('0'), max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='total profit')), ], options={ 'verbose_name': 'transaction item', @@ -259,6 +271,21 @@ class Migration(migrations.Migration): 'ordering': ['user', 'id'], }, ), + migrations.CreateModel( + name='ProgramProgress', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(verbose_name='date')), + ('description', django.contrib.postgres.fields.citext.CICharField(max_length=200, verbose_name='description')), + ('image', models.ImageField(blank=True, null=True, upload_to=api.utils.get_upload_file_path, verbose_name='image')), + ('program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program', to='api.Program', verbose_name='program')), + ], + options={ + 'verbose_name': 'program progress', + 'verbose_name_plural': 'program progress', + 'ordering': ['-date', 'id'], + }, + ), migrations.CreateModel( name='ProgramDonation', fields=[ @@ -276,6 +303,7 @@ class Migration(migrations.Migration): ('transfer_destination_bank_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='transfer destination bank name')), ('transfer_destination_bank_account_name', models.CharField(blank=True, max_length=200, null=True, verbose_name='transfer destination bank account name')), ('transfer_destination_bank_account_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='transfer destination bank account number')), + ('transfer_destination_bank_code_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='transfer destination bank code number')), ('goods_quantity', models.DecimalField(blank=True, decimal_places=0, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(Decimal('1'))], verbose_name='goods quantity')), ('goods_description', models.CharField(blank=True, max_length=200, null=True, verbose_name='goods description')), ('delivery_method', models.CharField(blank=True, choices=[('PCK', 'Pick Up'), ('DLV', 'Delivered')], max_length=3, null=True, verbose_name='goods delivery method')), @@ -302,6 +330,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')), ('quantity', models.PositiveIntegerField(default=0, verbose_name='quantity')), + ('hampers_messages', models.CharField(default='', max_length=100, verbose_name='hampers messages')), ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cart_items', to='api.Product', verbose_name='product')), ('shopping_cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cart_items', to='api.ShoppingCart', verbose_name='shopping cart')), ], diff --git a/api/migrations/0002_auto_20201229_1028.py b/api/migrations/0002_auto_20201229_1028.py deleted file mode 100644 index cfff94743344f0a16ba79e60d19b424fa7ba9fe0..0000000000000000000000000000000000000000 --- a/api/migrations/0002_auto_20201229_1028.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 3.0.7 on 2020-12-29 03:28 - -import api.utils -from decimal import Decimal -import django.contrib.postgres.fields.citext -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='product', - name='pre_order', - ), - migrations.RemoveField( - model_name='transactionitem', - name='product_pre_order', - ), - migrations.AddField( - model_name='product', - name='modal', - field=models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='modal'), - ), - migrations.AddField( - model_name='product', - name='profit', - field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='profit'), - ), - migrations.AddField( - model_name='product', - name='total_profit', - field=models.DecimalField(blank=True, decimal_places=2, default=Decimal('0'), max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='total profit'), - ), - migrations.CreateModel( - name='ProgramProgress', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateField(verbose_name='date')), - ('description', django.contrib.postgres.fields.citext.CICharField(max_length=200, verbose_name='description')), - ('image', models.ImageField(blank=True, null=True, upload_to=api.utils.get_upload_file_path, verbose_name='image')), - ('program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program', to='api.Program', verbose_name='program')), - ], - options={ - 'verbose_name': 'program progress', - 'verbose_name_plural': 'program progress', - 'ordering': ['-date', 'id'], - }, - ), - ] diff --git a/api/models.py b/api/models.py index 5ba7ff5c5b813554bcbe2055cdb5b1619fbfed23..e61c3eef53f469d9d4d9ae289d025712ee694a0a 100644 --- a/api/models.py +++ b/api/models.py @@ -53,6 +53,7 @@ class User(auth_models.AbstractUser): class BankAccountTransferDestination(db_models.Model): id = db_models.UUIDField(default=uuid.uuid4, primary_key=True, verbose_name=_('ID')) bank_name = db_models.CharField(max_length=100, verbose_name=_('bank name')) + bank_code_number = db_models.CharField(max_length=100, verbose_name=_('bank code number'), default='000') bank_account_number = db_models.CharField(max_length=100, verbose_name=_('bank account number')) bank_account_name = db_models.CharField(max_length=200, verbose_name=_('bank account name')) @@ -63,7 +64,7 @@ class BankAccountTransferDestination(db_models.Model): verbose_name_plural = _('bank account transfer destinations') def __str__(self): - return '{} - {}'.format(self.bank_name, self.bank_account_number) + return '{}: {} - {}'.format(self.bank_name, self.bank_code_number, self.bank_account_number) class Category(db_models.Model): @@ -155,7 +156,7 @@ class Product(db_models.Model): upload_to=utils.get_upload_file_path, verbose_name=_('image') ) - total_profit= db_models.DecimalField( + total_profit = db_models.DecimalField( blank=True, null=True, default=decimal.Decimal('0'), @@ -164,6 +165,22 @@ class Product(db_models.Model): validators=[validators.MinValueValidator(decimal.Decimal('0.01'))], verbose_name=_('total profit') ) + is_hampers = db_models.BooleanField(default=False, verbose_name=_('the product is hampers package')) + hampers_price = db_models.DecimalField( + decimal_places=2, + default=decimal.Decimal('0.00'), + max_digits=12, + validators=[validators.MinValueValidator(decimal.Decimal('0.00'))], + verbose_name=_('hampers price') + ) + unit = db_models.CharField(default='buah', max_length=200, verbose_name=_('unit')) + preorder = db_models.BooleanField(blank=True, null=True, default=False, verbose_name=_('preorder')) + preorder_duration = db_models.PositiveIntegerField( + blank=True, + null=True, + default=0, + verbose_name=_('preorder duration') + ) class Meta: ordering = ['subcategory', 'name', 'code', 'id'] verbose_name = _('product') @@ -201,6 +218,7 @@ class CartItem(db_models.Model): verbose_name=_('product') ) quantity = db_models.PositiveIntegerField(default=0, verbose_name=_('quantity')) + hampers_messages = db_models.CharField(default='', max_length=100, verbose_name=_('hampers messages')) class Meta: ordering = ['shopping_cart', 'product', 'id'] @@ -316,6 +334,12 @@ class Transaction(db_models.Model): null=True, verbose_name=_('transfer destination bank account number') ) + transfer_destination_bank_code_number = db_models.CharField( + blank=True, + max_length=100, + null=True, + verbose_name=_('transfer destination bank code number') + ) created_at = db_models.DateTimeField( auto_now_add=True, db_index=True, @@ -370,7 +394,24 @@ class TransactionItem(db_models.Model): validators=[validators.MinValueValidator(decimal.Decimal('0.01'))], verbose_name=_('product price') ) + profit= db_models.DecimalField( + blank=True, + null=True, + default=decimal.Decimal('0'), + decimal_places=2, + max_digits=12, + validators=[validators.MinValueValidator(decimal.Decimal('0.01'))], + verbose_name=_('total profit') + ) + hampers_price = db_models.DecimalField( + default=decimal.Decimal('0.00'), + decimal_places=2, + max_digits=12, + validators=[validators.MinValueValidator(decimal.Decimal('0.00'))], + verbose_name=_('hampers price') + ) + hampers_messages = db_models.CharField(default='', max_length=100, verbose_name=_('hampers messages')) quantity = db_models.PositiveIntegerField(verbose_name=_('quantity')) class Meta: @@ -524,6 +565,12 @@ class ProgramDonation(db_models.Model): max_length=100, verbose_name=_('transfer destination bank account number') ) + transfer_destination_bank_code_number = db_models.CharField( + blank=True, + null=True, + max_length=100, + verbose_name=_('transfer destination bank code number') + ) goods_quantity = db_models.DecimalField( blank=True, null=True, diff --git a/api/reports_writer.py b/api/reports_writer.py index 95a49ca29c8994433f4f37c8edce5f07874cc1bd..2b650f0c171f1c7f9aa258abefe48b1bd39d21ae 100644 --- a/api/reports_writer.py +++ b/api/reports_writer.py @@ -122,8 +122,8 @@ def transaction_or_donation_user_username_with_hyperlink(worksheet, row, col, ob def transaction_transaction_item_subtotal(worksheet, row, col, obj, cell_format): worksheet.write_number(row, col, sum( - transaction_item.product_price * transaction_item.quantity - for transaction_item in obj.transaction_items.all() + (transaction_item.product_price + transaction_item.hampers_price) + * transaction_item.quantity for transaction_item in obj.transaction_items.all() ), cell_format=cell_format) @@ -246,6 +246,8 @@ def create_transaction_report(filter_params): # pylint: disable=too-many-locals (transaction_item_transaction_transaction_number_with_hyperlink, _('Transaction Number')), ('product_name', _('Product Name')), ('product_price', _('Product Price')), + ('hampers_price', _('Hampers Price')), + ('hampers_messages', _('Hampers Messages')), ('quantity', _('Quantity')), ] write_queryset_data_to_worksheet( @@ -253,7 +255,10 @@ def create_transaction_report(filter_params): # pylint: disable=too-many-locals models.TransactionItem.objects.filter(transaction__in=transactions), transaction_item_fields, header_format=header_format, - data_format={'product_price': money_format}, + data_format={ + 'product_price': money_format, + 'hampers_price': money_format + }, col_width=32 ) product_order_summary_worksheet = workbook.add_worksheet(str(_('Product Order Summary'))) @@ -303,7 +308,7 @@ def create_transaction_report(filter_params): # pylint: disable=too-many-locals return buffer -def create_program_donation_report_CSH(filter_params): +def create_program_donation_report_csh(filter_params): buffer = io.BytesIO() workbook = xlsxwriter.Workbook( buffer, @@ -373,7 +378,7 @@ def create_program_donation_report_CSH(filter_params): workbook.close() return buffer -def create_program_donation_report_GDS(filter_params): +def create_program_donation_report_gds(filter_params): buffer = io.BytesIO() workbook = xlsxwriter.Workbook( buffer, diff --git a/api/seeds.py b/api/seeds.py index bcbb8fdba4f7031fef3a8c983a4c6e7a9885b221..6aaf429f30f08737781b395c7458c9452563e402 100644 --- a/api/seeds.py +++ b/api/seeds.py @@ -18,7 +18,8 @@ USER_DATA = { BANK_ACCOUNT_TRANSFER_DESTINATION = { 'bank_name': 'Dummy Bank Name', - 'bank_account_number': 'Dummy Bank Account Number', + 'bank_code_number': '011', + 'bank_account_number': '123456789', 'bank_account_name': 'Dummy Bank Account Name', } @@ -35,7 +36,12 @@ PRODUCT_DATA = { 'description': 'Dummy description.', 'price': '2000', 'stock': 10, - 'modal':'1000', + 'modal': '1000', + 'unit': 'kg', + 'is_hampers': True, + 'hampers_price': '500', + 'preorder' : True, + 'preorder_duration' : 5, } TRANSACTION_DATA = { @@ -45,6 +51,7 @@ TRANSACTION_DATA = { } TRANSACTION_ITEM_DATA = { + 'profit' : 1000, 'quantity': 1, } diff --git a/api/serializers.py b/api/serializers.py index 90263e533b3c63ae75447df34490a1ecdbef1556..7447690bdad6f8f58ecbf5a4366fc2ebb36b7357 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -20,6 +20,11 @@ class OTPSerializer(serializers.Serializer): # pylint: disable=abstract-method class CartUpdateSerializer(serializers.Serializer): # pylint: disable=abstract-method product = serializers.UUIDField(label=_('Product')) quantity = serializers.IntegerField(label=_('Quantity'), min_value=0) + hampers_messages = serializers.CharField( + label=_('Hampers Messages'), + max_length=100, + required=False + ) class CartCheckoutSerializer(serializers.Serializer): # pylint: disable=abstract-method @@ -223,7 +228,7 @@ class UserSerializer(serializers.ModelSerializer): class BankAccountTransferDestinationSerializer(serializers.ModelSerializer): class Meta: - fields = ['id', 'bank_name', 'bank_account_number', 'bank_account_name'] + fields = ['id', 'bank_name', 'bank_code_number', 'bank_account_number','bank_account_name'] model = models.BankAccountTransferDestination read_only_fields = ['id'] @@ -264,7 +269,12 @@ class ProductSerializer(serializers.ModelSerializer): 'modal', 'profit', 'image', - 'total_profit' + 'total_profit', + 'unit', + 'is_hampers', + 'hampers_price', + 'preorder', + 'preorder_duration' ] model = models.Product read_only_fields = ['id', 'code'] @@ -276,7 +286,7 @@ class CartItemSerializer(serializers.ModelSerializer): product = ProductSerializer(read_only=True) class Meta: - fields = ['id', 'product', 'quantity'] + fields = ['id', 'product', 'quantity', 'hampers_messages'] model = models.CartItem read_only_fields = ['id', 'quantity'] @@ -293,7 +303,8 @@ class ShoppingCartSerializer(serializers.ModelSerializer): def get_cart_item_subtotal(self, obj): # pylint: disable=no-self-use cart_item_subtotal = sum( - cart_item.product.price * cart_item.quantity for cart_item in obj.cart_items.all() + (cart_item.product.price + cart_item.product.hampers_price) + * cart_item.quantity for cart_item in obj.cart_items.all() ) return str(cart_item_subtotal) @@ -308,6 +319,8 @@ class TransactionItemSerializer(serializers.ModelSerializer): 'product_code', 'product_name', 'product_price', + 'hampers_price', + 'hampers_messages', 'quantity', ] model = models.TransactionItem @@ -316,6 +329,7 @@ class TransactionItemSerializer(serializers.ModelSerializer): 'product', 'product_name', 'product_price', + 'hampers_price', 'quantity', ] @@ -355,6 +369,7 @@ class TransactionSerializer(serializers.ModelSerializer): 'bank_account_transfer_destination', 'transfer_destination_bank_name', 'transfer_destination_bank_account_name', + 'transfer_destination_bank_code_number', 'transfer_destination_bank_account_number', 'created_at', 'updated_at', @@ -386,6 +401,7 @@ class TransactionSerializer(serializers.ModelSerializer): 'bank_account_transfer_destination', 'transfer_destination_bank_name', 'transfer_destination_bank_account_name', + 'transfer_destination_bank_code_number', 'transfer_destination_bank_account_number', 'created_at', 'updated_at', @@ -402,8 +418,8 @@ class TransactionSerializer(serializers.ModelSerializer): def get_transaction_item_subtotal(self, obj): # pylint: disable=no-self-use transaction_item_subtotal = sum( - transaction_item.product_price * transaction_item.quantity - for transaction_item in obj.transaction_items.all() + (transaction_item.product_price + transaction_item.hampers_price) + * transaction_item.quantity for transaction_item in obj.transaction_items.all() ) return str(transaction_item_subtotal) @@ -424,6 +440,7 @@ class TransactionSerializer(serializers.ModelSerializer): class ProgramSerializer(serializers.ModelSerializer): total_donation_amount = serializers.SerializerMethodField('get_total_donation_amount') + goods_donation = serializers.SerializerMethodField('get_list_goods_donation') class Meta: fields = [ @@ -440,6 +457,7 @@ class ProgramSerializer(serializers.ModelSerializer): 'poster_image', 'link', 'total_donation_amount', + 'goods_donation', ] model = models.Program read_only_fields = ['id', 'code'] @@ -452,6 +470,16 @@ class ProgramSerializer(serializers.ModelSerializer): ) ) return str(total_donation_amount) + def get_list_goods_donation(self, obj): + lst = obj.program_donations.filter( + donation_status='002', + donation_type='GDS' + ) + lst_ret = [] + for goods_donation in lst: + dct = {'id' : goods_donation.id, 'desc' : goods_donation.goods_description, 'quantity' : goods_donation.goods_quantity} + lst_ret.append(dct) + return lst_ret def validate(self, attrs): instance = self.instance @@ -484,7 +512,6 @@ class ProgramProgressSerializer(serializers.ModelSerializer): def validate(self, attrs): instance = self.instance - date = attrs.get('date', getattr(instance, 'date', None)) errors = {} if errors: raise serializers.ValidationError(errors) @@ -519,6 +546,7 @@ class ProgramDonationSerializer(serializers.ModelSerializer): 'bank_account_transfer_destination', 'transfer_destination_bank_name', 'transfer_destination_bank_account_name', + 'transfer_destination_bank_code_number', 'transfer_destination_bank_account_number', 'goods_quantity', 'goods_description', @@ -544,6 +572,7 @@ class ProgramDonationSerializer(serializers.ModelSerializer): 'bank_account_transfer_destination', 'transfer_destination_bank_name', 'transfer_destination_bank_account_name', + 'transfer_destination_bank_code_number', 'transfer_destination_bank_account_number', 'created_at', 'updated_at', diff --git a/api/signals.py b/api/signals.py index 6340fe60c31b773370fdbc71a5c09d8e5f7dfaaa..2f93db7681d6bba30ed04aab89129ca550c6ecb0 100644 --- a/api/signals.py +++ b/api/signals.py @@ -3,6 +3,7 @@ from django.db.models import signals from api import models, utils + @dispatch.receiver(signals.post_save, sender=models.User) def create_shopping_cart(sender, created, instance, **_kwargs): # pylint: disable=unused-argument if created: @@ -40,6 +41,7 @@ def fill_dependent_transaction_fields(sender, instance, **_kwargs): if instance.bank_account_transfer_destination is None: instance.transfer_destination_bank_name = None instance.transfer_destination_bank_account_name = None + instance.transfer_destination_bank_code_number = None instance.transfer_destination_bank_account_number = None else: instance.transfer_destination_bank_name = ( @@ -51,6 +53,9 @@ def fill_dependent_transaction_fields(sender, instance, **_kwargs): instance.transfer_destination_bank_account_number = ( instance.bank_account_transfer_destination.bank_account_number ) + instance.transfer_destination_bank_code_number = ( + instance.bank_account_transfer_destination.bank_code_number + ) if (instance.transaction_status == '002'): if instance.batch is None: @@ -71,10 +76,14 @@ def fill_dependent_transaction_item_fields(sender, instance, **_kwargs): if instance.product is None: instance.product_name = None instance.product_price = None + instance.hampers_price = None + instance.hampers_messages = None else: instance.product_name = instance.product.name instance.product_price = instance.product.price + instance.hampers_price = instance.hampers_price + instance.hampers_messages = instance.hampers_messages diff --git a/api/tests.py b/api/tests.py index fb6530a0b5d1fd0bd8e3c904c17d39c2a80458b7..4acc90ea20bbe4b5e297823878810d1662d0af20 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,16 +1,21 @@ +import datetime import decimal import tempfile +import uuid +from os import name from unittest import mock -import datetime + import jwt -from django import conf, test as django_test, urls +from django import conf +from django import test as django_test +from django import urls from django.core import management -from PIL import Image -from rest_framework import exceptions, status, test as rest_framework_test from django.utils import timezone -from api import models, seeds, utils, views -import uuid +from PIL import Image +from rest_framework import exceptions, status +from rest_framework import test as rest_framework_test +from api import models, seeds, utils, views def create_tmp_image(): @@ -298,6 +303,7 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst data = { 'product': self.product.id, 'quantity': 1, + 'hampers_messages': 'Selamat Lebaran', } response = request( 'POST', @@ -332,6 +338,7 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst data = { 'product': self.product.id, 'quantity': 1, + 'hampers_messages': 'Selamat Lebaran', } response = request( 'POST', @@ -386,6 +393,7 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst data = { 'product': self.product.id, 'quantity': 20, + 'hampers_messages': 'Selamat Lebaran', } request( 'POST', @@ -410,6 +418,7 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst data = { 'product': self.product.id, 'quantity': 1, + 'hampers_messages': 'Selamat Lebaran', } request( 'POST', @@ -454,6 +463,7 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst data = { 'product': self.product.id, 'quantity': 1, + 'hampers_messages': 'Selamat Lebaran', } request( 'POST', @@ -539,6 +549,7 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst data = { 'product': self.product.id, 'quantity': 20, + 'hampers_messages': 'Selamat Lebaran', } request( 'POST', @@ -855,8 +866,9 @@ class BankAccountTransferDestinationTest(rest_framework_test.APITestCase): ) self.assertEqual( str(bank_account_transfer_destination), - '{} - {}'.format( + '{}: {} - {}'.format( bank_account_transfer_destination.bank_name, + bank_account_transfer_destination.bank_code_number, bank_account_transfer_destination.bank_account_number ) ) @@ -891,6 +903,12 @@ class BankAccountTransferDestinationTest(rest_framework_test.APITestCase): ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(models.BankAccountTransferDestination.objects.count(), 1) + self.assertEqual( + models.BankAccountTransferDestination.objects.get( + id=response.data['id'] + ).bank_code_number, + data['bank_code_number'] + ) self.assertEqual( models.BankAccountTransferDestination.objects.get( id=response.data['id'] @@ -912,6 +930,7 @@ class BankAccountTransferDestinationTest(rest_framework_test.APITestCase): **seeds.BANK_ACCOUNT_TRANSFER_DESTINATION ) data = { + 'bank_code_number': '333', 'bank_account_number': 'Another Dummy Bank Account Number', } response = request( @@ -922,6 +941,12 @@ class BankAccountTransferDestinationTest(rest_framework_test.APITestCase): url_args=[bank_account_transfer_destination.id] ) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + models.BankAccountTransferDestination.objects.get( + id=bank_account_transfer_destination.id + ).bank_code_number, + data['bank_code_number'] + ) self.assertEqual( models.BankAccountTransferDestination.objects.get( id=bank_account_transfer_destination.id @@ -937,6 +962,12 @@ class BankAccountTransferDestinationTest(rest_framework_test.APITestCase): url_args=[bank_account_transfer_destination.id] ) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + models.BankAccountTransferDestination.objects.get( + id=bank_account_transfer_destination.id + ).bank_code_number, + data['bank_code_number'] + ) self.assertEqual( models.BankAccountTransferDestination.objects.get( id=bank_account_transfer_destination.id @@ -949,6 +980,7 @@ class BankAccountTransferDestinationTest(rest_framework_test.APITestCase): **seeds.BANK_ACCOUNT_TRANSFER_DESTINATION ) data = { + 'bank_code_number': '', 'bank_account_number': '', } response = request( @@ -1169,6 +1201,7 @@ class ProductTest(rest_framework_test.APITestCase): def test_create_product_success(self): data = seeds.PRODUCT_DATA data['subcategory']= self.subcategory.id + data['unit'] = 'kg' response = request( 'POST', @@ -1180,10 +1213,14 @@ class ProductTest(rest_framework_test.APITestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(models.Product.objects.count(), 1) self.assertEqual(models.Product.objects.get(id=response.data['id']).name, data['name']) + self.assertEqual(models.Product.objects.get(id=response.data['id']).unit, data['unit']) + self.assertEqual(models.Product.objects.get(id=response.data['id']).preorder, data['preorder']) + self.assertEqual(models.Product.objects.get(id=response.data['id']).preorder_duration, data['preorder_duration']) def test_create_product_fail(self): data = dict(seeds.PRODUCT_DATA, subcategory=self.subcategory.id) data['name'] = None + data['unit'] = None response = request( 'POST', 'product-list', @@ -1200,7 +1237,10 @@ class ProductTest(rest_framework_test.APITestCase): data = { 'name': 'Dummy', 'price':'4000', - 'modal':'2000' + 'modal':'2000', + 'unit': 'gram', + 'is_hampers': True, + 'hampers_price': '500', } response = request( 'PATCH', @@ -1212,6 +1252,8 @@ class ProductTest(rest_framework_test.APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(models.Product.objects.get(id=product.id).profit,2000) self.assertEqual(models.Product.objects.get(id=product.id).name, data['name']) + self.assertEqual(models.Product.objects.get(id=product.id).unit, data['unit']) + self.assertEqual(models.Product.objects.get(id=product.id).is_hampers, data['is_hampers']) data = dict(seeds.PRODUCT_DATA, subcategory=self.subcategory.id) response = request( 'PUT', @@ -1229,6 +1271,7 @@ class ProductTest(rest_framework_test.APITestCase): )) data = { 'name': '', + 'unit': '' } response = request( 'PATCH', @@ -1239,7 +1282,6 @@ class ProductTest(rest_framework_test.APITestCase): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - class ShoppingCartTest(rest_framework_test.APITestCase): def setUp(self): self.superuser = models.User.objects.create_superuser(**seeds.SUPERUSER_DATA) @@ -1432,6 +1474,14 @@ class TransactionItemTest(rest_framework_test.APITestCase): )) self.assertEqual(str(transaction_item), self.product.name) + def test_transaction_item_model_profit(self): + transaction_item = models.TransactionItem.objects.create(**dict( + seeds.TRANSACTION_ITEM_DATA, + transaction=self.transaction, + product=self.product + )) + self.assertEqual(transaction_item.profit, 1000) + class ProgramTest(rest_framework_test.APITestCase): def setUp(self): @@ -1629,6 +1679,21 @@ class ProgramDonationTest(rest_framework_test.APITestCase): url_args=[program_donation.id] ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_delete_by_program(self): + program = models.Program.objects.create(**seeds.PROGRAM_DATA ) + donation = models.ProgramDonation.objects.create(**dict( + seeds.PROGRAM_DONATION_CASH_DATA, + user=self.user, + program=program)) + response = request( + 'DELETE', + 'donation-by-program', + http_authorization=self.superuser_http_authorization, + url_args=[program.id] + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(models.ProgramDonation.objects.count(), 0) + class ChoicesViewsTest(rest_framework_test.APITestCase): diff --git a/api/urls.py b/api/urls.py index c47bffd1a9910b47cf21528a22c4d82b0e04726a..9feba438633d339bcf2440d842aad35de09da834 100644 --- a/api/urls.py +++ b/api/urls.py @@ -30,6 +30,7 @@ urlpatterns = [ name='cart-cancel-transaction' ), urls.path('donation/create/', api_views.DonationCreate.as_view(), name='donation-create'), + urls.path('donation/delete-by-p/', api_views.delete_donation_by_program, name='donation-by-program'), urls.path( 'donation/reupload-proof-of-bank-transfer/', api_views.DonationReuploadProofOfBankTransfer.as_view(), diff --git a/api/views.py b/api/views.py deleted file mode 100644 index dc62ed4007187378a1ea6ad3315ab042f08d1bff..0000000000000000000000000000000000000000 --- a/api/views.py +++ /dev/null @@ -1,1027 +0,0 @@ - -from django import http, shortcuts -from django.contrib import auth -from django.db import transaction as db_transaction, utils as db_utils -from django.db.models import deletion -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ -from django_filters import rest_framework -from knox import views as knox_views -from rest_framework import ( - exceptions as rest_framework_exceptions, filters as rest_framework_filters, generics, - permissions as rest_framework_permissions, response, status, views as rest_framework_views -) -from rest_framework.authtoken import serializers as authtoken_serializers - -from api import ( - constants, exceptions as api_exceptions, filters as api_filters, models, paginations, - permissions as api_permissions, reports_writer, schemas, serializers as api_serializers, - utils as api_utils -) -from home_industry import utils as home_industry_utils - - -class AuthRegister(generics.CreateAPIView): - permission_classes = [api_permissions.IsAnonymousUser] - serializer_class = api_serializers.UserSerializer - - -class AuthCredLogin(knox_views.LoginView): - permission_classes = [rest_framework_permissions.AllowAny] - serializer_class = authtoken_serializers.AuthTokenSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, format=None): # pylint: disable=redefined-builtin - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = serializer.validated_data['user'] - auth.login(request, user) - return super().post(request, format) - - -class AuthPhoneNumberLogin(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.AllowAny] - serializer_class = api_serializers.PhoneNumberSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = shortcuts.get_object_or_404( - models.User, - phone_number=serializer.validated_data['phone_number'] - ) - user.otp = api_utils.generate_otp() - user.save() - api_utils.send_otp(str(user.phone_number), user.otp) - token = api_utils.generate_bearer_token(user) - return response.Response({'token': token}, status=status.HTTP_200_OK) - - -class AuthOTPLogin(knox_views.LoginView): - permission_classes = [rest_framework_permissions.AllowAny] - serializer_class = api_serializers.OTPSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, format=None): # pylint: disable=redefined-builtin - http_authorization = self.request.META.get('HTTP_AUTHORIZATION') - username = api_utils.get_username_from_bearer_token(http_authorization) - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = shortcuts.get_object_or_404(models.User, username=username) - if user.otp != serializer.validated_data['otp']: - return response.Response(status=status.HTTP_400_BAD_REQUEST) - auth.login(request, user) - user.otp = '' - user.save() - return super().post(request, format) - - -class AuthResendOTP(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.AllowAny] - - def post(self, request, _format=None): - http_authorization = self.request.META.get('HTTP_AUTHORIZATION') - username = api_utils.get_username_from_bearer_token(http_authorization) - user = shortcuts.get_object_or_404(models.User, username=username) - user.otp = api_utils.generate_otp() - user.save() - api_utils.send_otp(str(user.phone_number), user.otp) - return response.Response(status=status.HTTP_204_NO_CONTENT) - - -class CartUpdate(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - serializer_class = api_serializers.CartUpdateSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = request.user - product = shortcuts.get_object_or_404( - models.Product, - id=serializer.validated_data['product'] - ) - shopping_cart = models.ShoppingCart.objects.get(user=user) - cart_item, _created = models.CartItem.objects.get_or_create( - product=product, - shopping_cart=shopping_cart - ) - if serializer.validated_data['quantity'] == 0: - cart_item.delete() - else: - cart_item.quantity = serializer.validated_data['quantity'] - cart_item.save() - return response.Response(status=status.HTTP_204_NO_CONTENT) - - -class CartOverview(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - - def get(self, request, _format=None): - user = request.user - shopping_cart = models.ShoppingCart.objects.get(user=user) - item_subtotal = sum( - cart_item.product.price * cart_item.quantity - for cart_item in shopping_cart.cart_items.all() - ) - shipping_costs = api_utils.get_shipping_costs(user) - if shipping_costs is None: - return response.Response( - {'item_subtotal': str(item_subtotal)}, - status=status.HTTP_200_OK - ) - return response.Response( - {'item_subtotal': str(item_subtotal), 'shipping_costs': str(shipping_costs)}, - status=status.HTTP_200_OK - ) - - -class CartCheckout(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - serializer_class = api_serializers.CartCheckoutSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = request.user - shipment_config = home_industry_utils.get_shipment_config() - if user.sub_district.lower() != shipment_config.sub_district.lower(): - raise rest_framework_exceptions.ParseError(_( # pylint: disable=no-member - 'Cannot process shipment to other sub-districts other than {sub_district}.' - ).format(sub_district=shipment_config.sub_district)) - shopping_cart = models.ShoppingCart.objects.get(user=user) - cart_items = shopping_cart.cart_items.all() - if not cart_items.exists(): - raise rest_framework_exceptions.ParseError(_( - 'Unable to checkout because there are no items purchased.' - )) - api_utils.validate_product_stock(cart_items) - transaction_status = ( - '001' if serializer.validated_data['payment_method'] == 'TRF' else '002' - ) - - transaction = models.Transaction.objects.create( - user=user, - payment_method=serializer.validated_data['payment_method'], - donation=serializer.validated_data['donation'], - transaction_status=transaction_status, - batch = ( - None if serializer.validated_data['payment_method'] == 'TRF' - else models.Batch.objects.filter( - start_date__lte=timezone.now().date(), - end_date__gte=timezone.now().date()).first() - ) - ) - - is_success = True - for cart_item in cart_items: - product = cart_item.product - if product.stock is not None: - try: - with db_transaction.atomic(): - product.stock -= cart_item.quantity - product.total_profit += cart_item.quantity*product.profit - product.save() - except db_utils.IntegrityError: - is_success = False - models.TransactionItem.objects.create( - transaction=transaction, - product=product, - quantity=cart_item.quantity - ) - cart_item.delete() - if not is_success: - transaction.transaction_status = '007' - transaction.save() - raise rest_framework_exceptions.APIException(_('Checkout failed.')) - return response.Response({'transaction': transaction.id}, status=status.HTTP_200_OK) - - -class CartUploadPOP(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - serializer_class = api_serializers.CartUploadPOPSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = request.user - bank_account_transfer_destination = shortcuts.get_object_or_404( - models.BankAccountTransferDestination, - id=serializer.validated_data['bank_account_transfer_destination'] - ) - transaction = shortcuts.get_object_or_404( - models.Transaction, - id=serializer.validated_data['transaction'], - user=user - ) - if transaction.payment_method != 'TRF': - raise rest_framework_exceptions.PermissionDenied(_( - 'The payment method for this transaction is not a transfer.' - )) - if transaction.transaction_status not in ('001', '002'): - raise rest_framework_exceptions.PermissionDenied(_( - 'Cannot upload proof of payment at this stage.' - )) - transaction.proof_of_payment = serializer.validated_data['proof_of_payment'] - transaction.user_bank_name = serializer.validated_data['user_bank_name'] - transaction.user_bank_account_name = serializer.validated_data['user_bank_account_name'] - transaction.bank_account_transfer_destination = bank_account_transfer_destination - transaction.update_bank_account_transfer_destination = True - transaction.transaction_status = '002' - transaction.batch = models.Batch.objects.filter( - start_date__lte=timezone.now().date(), - end_date__gte=timezone.now().date()).first() - transaction.save() - return response.Response(status=status.HTTP_204_NO_CONTENT) - - -class CartCompleteTransaction(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - serializer_class = api_serializers.CartCompleteOrCancelTransactionSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - transaction = shortcuts.get_object_or_404( - models.Transaction, - id=serializer.validated_data['transaction'], - user=request.user - ) - if transaction.transaction_status != '004': - raise rest_framework_exceptions.PermissionDenied(_( - 'Transaction cannot be completed unless the status is "Being shipped".' - )) - transaction.transaction_status = '005' - transaction.save() - return response.Response(status=status.HTTP_204_NO_CONTENT) - - -class CartCancelTransaction(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - serializer_class = api_serializers.CartCompleteOrCancelTransactionSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = request.user - transaction = shortcuts.get_object_or_404( - models.Transaction, - id=serializer.validated_data['transaction'], - user=user - ) - if transaction.transaction_status not in ('001', '002'): - raise rest_framework_exceptions.PermissionDenied(_( - 'Transaction cannot be canceled at this stage.' - )) - transaction_items = transaction.transaction_items.all() - api_utils.return_transaction_items_to_product_stock(transaction_items) - transaction.transaction_status = '006' - transaction.save() - return response.Response(status=status.HTTP_204_NO_CONTENT) - - -class DonationCreate(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - serializer_class = api_serializers.DonationCreateSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = request.user - program = shortcuts.get_object_or_404( - models.Program, - id=serializer.validated_data['program'] - ) - if not program.open_donation: - raise rest_framework_exceptions.PermissionDenied(_( - 'This program is currently not accepting donations.' - )) - program_donation = None - if serializer.validated_data['donation_type'] == 'CSH': - bank_account_transfer_destination = shortcuts.get_object_or_404( - models.BankAccountTransferDestination, - id=serializer.validated_data['bank_account_transfer_destination'] - ) - program_donation = models.ProgramDonation.objects.create( - user=user, - program=program, - donation_type='CSH', - amount=serializer.validated_data['amount'], - proof_of_bank_transfer=serializer.validated_data['proof_of_bank_transfer'], - user_bank_name=serializer.validated_data['user_bank_name'], - user_bank_account_name=serializer.validated_data['user_bank_account_name'], - bank_account_transfer_destination=bank_account_transfer_destination - ) - - else: - if serializer.validated_data['delivery_method'] == 'DLV': - program_donation = models.ProgramDonation.objects.create( - user=user, - program=program, - donation_type='GDS', - goods_quantity=serializer.validated_data['goods_quantity'], - goods_description=serializer.validated_data['goods_description'], - delivery_method=serializer.validated_data['delivery_method'], - delivery_address=None - ) - else: - program_donation = models.ProgramDonation.objects.create( - user=user, - program=program, - donation_type='GDS', - goods_quantity=serializer.validated_data['goods_quantity'], - goods_description=serializer.validated_data['goods_description'], - delivery_method=serializer.validated_data['delivery_method'], - delivery_address=serializer.validated_data['delivery_address'] - ) - - return response.Response( - {'program_donation': program_donation.id}, - status=status.HTTP_200_OK - ) - - - -class DonationReuploadProofOfBankTransfer(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - serializer_class = api_serializers.DonationReuploadProofOfBankTransferSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = request.user - bank_account_transfer_destination = shortcuts.get_object_or_404( - models.BankAccountTransferDestination, - id=serializer.validated_data['bank_account_transfer_destination'] - ) - program_donation = shortcuts.get_object_or_404( - models.ProgramDonation, - id=serializer.validated_data['program_donation'], - user=user - ) - if program_donation.donation_status not in ('001', '004'): - raise rest_framework_exceptions.PermissionDenied(_( - 'Cannot reupload proof of bank transfer at this stage.' - )) - if program_donation.donation_type != 'CSH': - raise rest_framework_exceptions.PermissionDenied(_( - 'Cannot proof of bank transfer foor good donation.' - )) - program_donation.amount = serializer.validated_data['amount'] - program_donation.proof_of_bank_transfer = ( - serializer.validated_data['proof_of_bank_transfer'] - ) - program_donation.user_bank_name = serializer.validated_data['user_bank_name'] - program_donation.user_bank_account_name = ( - serializer.validated_data['user_bank_account_name'] - ) - program_donation.bank_account_transfer_destination = bank_account_transfer_destination - program_donation.update_bank_account_transfer_destination = True - program_donation.donation_status = '001' - program_donation.save() - return response.Response(status=status.HTTP_204_NO_CONTENT) - - -class ReportAPIView(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAdminUser] - report_function = None - serializer_class = None - - def get_filename(self, query_params): - raise NotImplementedError - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) # pylint: disable=not-callable - - def get(self, request, _format=None): - assert self.report_function is not None, ( - '{} should include a `report_function` attribute.'.format(self.__class__.__name__) - ) - assert self.report_function is not None, ( - '{} should include a `serializer_class` attribute.'.format(self.__class__.__name__) - ) - serializer = self.get_serializer(data=request.query_params) - serializer.is_valid(raise_exception=True) - buffer = self.report_function.__func__(serializer.validated_data) - buffer.seek(0) - return http.FileResponse(buffer, filename=self.get_filename(serializer.validated_data)) - - -class ReportTransaction(ReportAPIView): - report_function = reports_writer.create_transaction_report - schema = schemas.ReportTransactionSchema() - serializer_class = api_serializers.ReportTransactionSerializer - - def get_filename(self, query_params): - filename = '{}.xlsx'.format(_( # pylint: disable=no-member - 'Transaction Report from {date_from} to {date_to} for {batch_name}' - ).format( - date_from=query_params.get('created_at_date_range_after', '_'), - date_to=query_params.get('created_at_date_range_before', str(timezone.now())[:10]), - batch_name=query_params.get('batch_name', '_') - )) - return filename - - -class ReportProgramDonationCSH(ReportAPIView): - report_function = reports_writer.create_program_donation_report_CSH - schema = schemas.ReportProgramDonationSchema() - serializer_class = api_serializers.ReportProgramDonationSerializer - - def get_filename(self, query_params): - filename = '{}.xlsx'.format(_( # pylint: disable=no-member - 'Program Donation Report from {date_from} to {date_to}' - ).format( - date_from=query_params.get('created_at_date_range_after', '_'), - date_to=query_params.get('created_at_date_range_before', str(timezone.now())[:10]) - )) - return filename - -class ReportProgramDonationGDS(ReportAPIView): - report_function = reports_writer.create_program_donation_report_GDS - schema = schemas.ReportProgramDonationSchema() - serializer_class = api_serializers.ReportProgramDonationSerializer - - def get_filename(self, query_params): - filename = '{}.xlsx'.format(_( # pylint: disable=no-member - 'Program Donation Report from {date_from} to {date_to}' - ).format( - date_from=query_params.get('created_at_date_range_after', '_'), - date_to=query_params.get('created_at_date_range_before', str(timezone.now())[:10]) - )) - return filename - - -class UserList(generics.ListCreateAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_fields = ['username', 'phone_number'] - ordering_fields = [ - 'username', 'full_name', 'phone_number', - 'total_transactions', 'total_program_donations_goods', 'total_program_donations_cash' - ] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [rest_framework_permissions.IsAdminUser] - queryset = models.User.objects.all() - search_fields = ['username', 'full_name', 'phone_number'] - serializer_class = api_serializers.UserSerializer - -class UserDetail(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrSelf, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.User.objects.all() - serializer_class = api_serializers.UserSerializer - - def get_object(self): - if self.kwargs.get('pk') == 'self': - self.kwargs['pk'] = self.request.user.id - return super().get_object() - - -class BankAccountTransferDestinationList(generics.ListCreateAPIView): - filter_backends = [rest_framework_filters.SearchFilter] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.BankAccountTransferDestination.objects.all() - search_fields = ['bank_name', 'bank_account_number', 'bank_account_name'] - serializer_class = api_serializers.BankAccountTransferDestinationSerializer - - -class BankAccountTransferDestinationDetail(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.BankAccountTransferDestination.objects.all() - serializer_class = api_serializers.BankAccountTransferDestinationSerializer - - -class CategoryList(generics.ListCreateAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_fields = ['name'] - ordering_fields = ['name'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Category.objects.all() - search_fields = ['name'] - serializer_class = api_serializers.CategorySerializer - - -class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Category.objects.all() - serializer_class = api_serializers.CategorySerializer - - def destroy(self, request, *_args, **_kwargs): - instance = self.get_object() - try: - instance.delete() - except deletion.ProtectedError: - raise api_exceptions.IntegrityError(_( - 'Cannot delete category due to integrity error.' - )) - return response.Response(status=status.HTTP_204_NO_CONTENT) - - -class SubcategoryList(generics.ListCreateAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_fields = ['name', 'category'] - ordering_fields = ['name'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Subcategory.objects.all() - search_fields = ['name'] - serializer_class = api_serializers.SubcategorySerializer - - -class SubcategoryDetail(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Subcategory.objects.all() - serializer_class = api_serializers.SubcategorySerializer - - def destroy(self, request, *_args, **_kwargs): - instance = self.get_object() - try: - instance.delete() - except deletion.ProtectedError: - raise api_exceptions.IntegrityError(_( - 'Cannot delete subcategory due to integrity error.' - )) - return response.Response(status=status.HTTP_204_NO_CONTENT) - - -class ProductList(generics.ListCreateAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_fields = ['code', 'subcategory', 'subcategory__category'] - ordering_fields = ['name', 'price', 'stock'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Product.objects.all() - search_fields = ['code', 'name'] - serializer_class = api_serializers.ProductSerializer - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - product = models.Product.objects.create( - name=serializer.validated_data['name'], - description=serializer.validated_data['description'], - price=serializer.validated_data['price'], - stock=serializer.validated_data['stock'], - modal=serializer.validated_data['modal'], - subcategory=models.Subcategory.objects.get(name=serializer.validated_data['subcategory']), - total_profit=0 - ) - product.profit= (product.price - product.modal) - product.save() - return response.Response( - {'id': product.id}, - status=status.HTTP_201_CREATED - ) - - -class ProductDetail(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Product.objects.all() - serializer_class = api_serializers.ProductSerializer - - def get_serializer(self, *args, **kwargs): - # leave this intact - serializer_class = self.get_serializer_class() - kwargs["context"] = self.get_serializer_context() - - - - if (self.request != None): - if (self.request.data.get("price") != None) and (self.request.data.get("modal") != None): - draft_request_data = self.request.data.copy() - profit= int(self.request.data['price'])-int(self.request.data['modal']) - new_profit={'profit': profit} - draft_request_data.update(new_profit) - kwargs["data"] = draft_request_data - return serializer_class(*args, **kwargs) - else: - if (self.request.data.get("price") != None): - - instance=self.get_object() - draft_request_data = self.request.data.copy() - profit= int(self.request.data['price'])-int(instance.modal) - new_profit={'profit': profit} - draft_request_data.update(new_profit) - - kwargs["data"] = draft_request_data - return serializer_class(*args, **kwargs) - elif (self.request.data.get("modal") != None): - instance=self.get_object() - draft_request_data = self.request.data.copy() - profit= int(instance.price)-int(self.request.data['modal']) - new_profit={'profit': profit} - draft_request_data.update(new_profit) - - kwargs["data"] = draft_request_data - return serializer_class(*args, **kwargs) - - return serializer_class(*args, **kwargs) - - def partial_update(self, request, *args, **kwargs): - instance = self.get_object() - - serializer = self.get_serializer(instance,data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - - return super().partial_update(request, *args, **kwargs) # pylint: disable=no-member - - - -class ProgramProgressList(generics.ListCreateAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - lookup_field = 'program' - serializer_class = api_serializers.ProgramProgressSerializer - - def get_queryset(self): - return models.ProgramProgress.objects.filter(program=self.get_program()) - - def perform_create(self, serializer): - serializer.save(program=self.get_program()) - - def get_program(self): - program = models.Program.objects.filter(id=self.kwargs.get(self.lookup_field)).first() - if program is None: - raise rest_framework_exceptions.NotFound(_( - 'Program not found.' - )) - return program - - -class ShoppingCartList(generics.ListAPIView): - filter_backends = [rest_framework.DjangoFilterBackend] - filterset_fields = ['user'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [rest_framework_permissions.IsAdminUser] - queryset = models.ShoppingCart.objects.all() - serializer_class = api_serializers.ShoppingCartSerializer - - -class ShoppingCartDetail(generics.RetrieveAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrOwner, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.ShoppingCart.objects.all() - serializer_class = api_serializers.ShoppingCartSerializer - - def get_object(self): - if self.kwargs.get('pk') == 'self': - self.kwargs['pk'] = models.ShoppingCart.objects.get(user=self.request.user).id - return super().get_object() - - -class TransactionList(generics.ListAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_class = api_filters.TransactionFilter - ordering_fields = ['created_at', 'updated_at'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [rest_framework_permissions.IsAuthenticated] - queryset = models.Transaction.objects.all() - schema = schemas.TransactionListSchema() - search_fields = ['transaction_number', 'user_full_name'] - serializer_class = api_serializers.TransactionSerializer - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - if not self.request.user.is_staff: - return queryset.filter(user=self.request.user) - return queryset - - -class TransactionDetail(generics.RetrieveUpdateAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrOwnerReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Transaction.objects.all() - serializer_class = api_serializers.TransactionSerializer - - def update(self, request, *args, **kwargs): - instance = self.get_object() - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - if instance.transaction_status in ('005', '006', '007'): - raise rest_framework_exceptions.PermissionDenied(_( - 'Cannot update transaction because it has a completed, canceled, or failed status.' - )) - if serializer.validated_data['transaction_status'] == '006': - transaction_items = instance.transaction_items.all() - api_utils.return_transaction_items_to_product_stock(transaction_items) - return super().update(request, *args, **kwargs) # pylint: disable=no-member - - -class ProgramList(generics.ListCreateAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_fields = ['code'] - ordering_fields = ['name', 'start_date_time', 'end_date_time'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Program.objects.all() - search_fields = ['code', 'name'] - serializer_class = api_serializers.ProgramSerializer - - -class ProgramDetail(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Program.objects.all() - serializer_class = api_serializers.ProgramSerializer - -class ProgramDonationList(generics.ListAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_class = api_filters.ProgramDonationFilter - ordering_fields = ['created_at', 'updated_at'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [rest_framework_permissions.IsAuthenticated] - queryset = models.ProgramDonation.objects.all() - schema = schemas.ProgramDonationListSchema() - search_fields = ['donation_number', 'user_full_name', 'program_name', 'donation_type'] - serializer_class = api_serializers.ProgramDonationSerializer - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - if not self.request.user.is_staff: - return queryset.filter(user=self.request.user) - return queryset - - -class ProgramDonationListCSH(generics.ListAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_class = api_filters.ProgramDonationFilter - ordering_fields = ['created_at', 'updated_at'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [rest_framework_permissions.IsAuthenticated] - queryset = models.ProgramDonation.objects.filter(donation_type='CSH') - schema = schemas.ProgramDonationListSchema() - search_fields = ['donation_number', 'user_full_name', 'program_name', 'donation_type'] - serializer_class = api_serializers.ProgramDonationSerializer - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - if not self.request.user.is_staff: - return queryset.filter(user=self.request.user) - return queryset - -class ProgramDonationListGDS(generics.ListAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - filterset_class = api_filters.ProgramDonationFilter - ordering_fields = ['created_at', 'updated_at'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [rest_framework_permissions.IsAuthenticated] - queryset = models.ProgramDonation.objects.filter(donation_type='GDS') - schema = schemas.ProgramDonationListSchema() - search_fields = ['donation_number', 'user_full_name', 'program_name', 'donation_type'] - serializer_class = api_serializers.ProgramDonationSerializer - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - if not self.request.user.is_staff: - return queryset.filter(user=self.request.user) - return queryset - -class ProgramDonationDetail(generics.RetrieveUpdateAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrOwnerReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.ProgramDonation.objects.all() - serializer_class = api_serializers.ProgramDonationSerializer - - def update(self, request, *args, **kwargs): - instance = self.get_object() - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - if instance.donation_status in ('002', '003'): - raise rest_framework_exceptions.PermissionDenied(_( - 'Cannot update program donation because it has a completed or canceled status.' - )) - return super().update(request, *args, **kwargs) # pylint: disable=no-member - - -class ChoicesAPIView(rest_framework_views.APIView): - choices = None - permission_classes = [rest_framework_permissions.IsAuthenticated] - - def get(self, request, _format=None): - assert self.choices is not None, ( - '{} should include a `choices` attribute.'.format(self.__class__.__name__) - ) - return response.Response(api_utils.map_choices(self.choices), status=status.HTTP_200_OK) - - -class PaymentMethodChoices(ChoicesAPIView): - choices = constants.PAYMENT_METHOD_CHOICES - - -class TransactionStatusChoices(ChoicesAPIView): - choices = constants.TRANSACTION_STATUS_CHOICES - - -class DonationStatusChoices(ChoicesAPIView): - choices = constants.DONATION_STATUS_CHOICES - - -class AppConfigDetail(generics.RetrieveUpdateAPIView): - permission_classes = [rest_framework_permissions.IsAdminUser] - queryset = models.AppConfig.objects.all() - serializer_class = api_serializers.AppConfigSerializer - - def get_object(self): - obj = shortcuts.get_object_or_404(models.AppConfig) - return obj - - -class HelpContactConfigDetail(generics.RetrieveUpdateAPIView): - permission_classes = [api_permissions.IsAdminUserOrReadOnly] - queryset = models.HelpContactConfig.objects.all() - serializer_class = api_serializers.HelpContactConfigSerializer - - def get_object(self): - obj = shortcuts.get_object_or_404(models.HelpContactConfig) - return obj - - -class ShipmentConfigDetail(generics.RetrieveUpdateAPIView): - permission_classes = [api_permissions.IsAdminUserOrReadOnly] - queryset = models.ShipmentConfig.objects.all() - serializer_class = api_serializers.ShipmentConfigSerializer - - def get_object(self): - obj = shortcuts.get_object_or_404(models.ShipmentConfig) - return obj - -class BatchList(generics.ListCreateAPIView): - filter_backends = [ - rest_framework.DjangoFilterBackend, - rest_framework_filters.OrderingFilter, - rest_framework_filters.SearchFilter, - ] - - filterset_class = api_filters.BatchFilter - ordering_fields = ['created_at', 'updated_at'] - pagination_class = paginations.SmallResultsSetPagination - permission_classes = [rest_framework_permissions.IsAuthenticated] - queryset = models.Batch.objects.all() - schema = schemas.BatchListSchema() - search_fields = ['batch_name'] - serializer_class = api_serializers.BatchSerializer - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - if serializer.validated_data['start_date'] < serializer.validated_data['end_date']: - batch = models.Batch.objects.create( - batch_name=serializer.validated_data['batch_name'], - start_date=serializer.validated_data['start_date'], - end_date=serializer.validated_data['end_date'], - shipping_cost=serializer.validated_data['shipping_cost'], - ) - else: - raise rest_framework_exceptions.PermissionDenied(_( - 'Start Date must be earlier than End Date.' - )) - return response.Response( - {'id': batch.id}, - status=status.HTTP_201_CREATED - ) - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - if not self.request.user.is_staff: - return queryset.filter(user=self.request.user) - return queryset - -class BatchDetail(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [ - api_permissions.IsAdminUserOrReadOnly, - rest_framework_permissions.IsAuthenticated, - ] - queryset = models.Batch.objects.all() - serializer_class = api_serializers.BatchSerializer - -class BatchCreate(rest_framework_views.APIView): - permission_classes = [rest_framework_permissions.IsAuthenticated] - serializer_class = api_serializers.BatchCreateSerializer - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - def post(self, request, _format=None): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - if serializer.validated_data['start_date'] < serializer.validated_date['end_date']: - batch = models.Batch.objects.create( - batch_name=serializer.validated_data['batch_name'], - start_date=serializer.validated_data['start_date'], - end_date=serializer.validated_data['end_date'], - shipping_cost=serializer.validated_data['shipping_cost'], - ) - else: - raise rest_framework_exceptions.PermissionDenied(_( - 'Start Date must be earlier than End Date.' - )) - return response.Response( - {'id': batch.id}, - status=status.HTTP_201_CREATED - ) diff --git a/api/views/__init__.py b/api/views/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..612dc853771037af3a572e7f39e5637cd4b1b9b8 --- /dev/null +++ b/api/views/__init__.py @@ -0,0 +1,12 @@ +from .auth import * +from .bank import * +from .batch import * +from .cart import * +from .choices import * +from .config import * +from .donation import * +from .product import * +from .program import * +from .report import * +from .transaction import * +from .user import * \ No newline at end of file diff --git a/api/views/auth.py b/api/views/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..764a741b62f5b61d9951aa2dc975bfc6454adf25 --- /dev/null +++ b/api/views/auth.py @@ -0,0 +1,87 @@ +from api import models +from api import permissions as api_permissions +from api import serializers as api_serializers +from api import utils as api_utils +from django import shortcuts +from django.contrib import auth +from knox import views as knox_views +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions +from rest_framework import response, status +from rest_framework import views as rest_framework_views +from rest_framework.authtoken import serializers as authtoken_serializers + + +class AuthRegister(generics.CreateAPIView): + permission_classes = [api_permissions.IsAnonymousUser] + serializer_class = api_serializers.UserSerializer + + +class AuthCredLogin(knox_views.LoginView): + permission_classes = [rest_framework_permissions.AllowAny] + serializer_class = authtoken_serializers.AuthTokenSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, format=None): # pylint: disable=redefined-builtin + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + auth.login(request, user) + return super().post(request, format) + + +class AuthPhoneNumberLogin(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.AllowAny] + serializer_class = api_serializers.PhoneNumberSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = shortcuts.get_object_or_404( + models.User, + phone_number=serializer.validated_data['phone_number'] + ) + user.otp = api_utils.generate_otp() + user.save() + api_utils.send_otp(str(user.phone_number), user.otp) + token = api_utils.generate_bearer_token(user) + return response.Response({'token': token}, status=status.HTTP_200_OK) + + +class AuthOTPLogin(knox_views.LoginView): + permission_classes = [rest_framework_permissions.AllowAny] + serializer_class = api_serializers.OTPSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, format=None): # pylint: disable=redefined-builtin + http_authorization = self.request.META.get('HTTP_AUTHORIZATION') + username = api_utils.get_username_from_bearer_token(http_authorization) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = shortcuts.get_object_or_404(models.User, username=username) + if user.otp != serializer.validated_data['otp']: + return response.Response(status=status.HTTP_400_BAD_REQUEST) + auth.login(request, user) + user.otp = '' + user.save() + return super().post(request, format) + + +class AuthResendOTP(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.AllowAny] + + def post(self, request, _format=None): + http_authorization = self.request.META.get('HTTP_AUTHORIZATION') + username = api_utils.get_username_from_bearer_token(http_authorization) + user = shortcuts.get_object_or_404(models.User, username=username) + user.otp = api_utils.generate_otp() + user.save() + api_utils.send_otp(str(user.phone_number), user.otp) + return response.Response(status=status.HTTP_204_NO_CONTENT) diff --git a/api/views/bank.py b/api/views/bank.py new file mode 100644 index 0000000000000000000000000000000000000000..20ddfcfd82e616638a6c9d99e5bc5ddba0e82d43 --- /dev/null +++ b/api/views/bank.py @@ -0,0 +1,27 @@ +from api import models, paginations +from api import permissions as api_permissions +from api import serializers as api_serializers +from rest_framework import filters as rest_framework_filters +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions + + +class BankAccountTransferDestinationList(generics.ListCreateAPIView): + filter_backends = [rest_framework_filters.SearchFilter] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.BankAccountTransferDestination.objects.all() + search_fields = ['bank_name', 'bank_account_number', 'bank_account_name'] + serializer_class = api_serializers.BankAccountTransferDestinationSerializer + + +class BankAccountTransferDestinationDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.BankAccountTransferDestination.objects.all() + serializer_class = api_serializers.BankAccountTransferDestinationSerializer diff --git a/api/views/batch.py b/api/views/batch.py new file mode 100644 index 0000000000000000000000000000000000000000..f8de732005b7131b85e86b0f3bd282cb39316de5 --- /dev/null +++ b/api/views/batch.py @@ -0,0 +1,92 @@ +from api import filters as api_filters +from api import models, paginations +from api import permissions as api_permissions +from api import schemas +from api import serializers as api_serializers +from django.utils.translation import gettext_lazy as _ +from django_filters import rest_framework +from rest_framework import exceptions as rest_framework_exceptions +from rest_framework import filters as rest_framework_filters +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions +from rest_framework import response, status +from rest_framework import views as rest_framework_views + + +class BatchList(generics.ListCreateAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + + filterset_class = api_filters.BatchFilter + ordering_fields = ['created_at', 'updated_at'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [rest_framework_permissions.IsAuthenticated] + queryset = models.Batch.objects.all() + schema = schemas.BatchListSchema() + search_fields = ['batch_name'] + serializer_class = api_serializers.BatchSerializer + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + if serializer.validated_data['start_date'] < serializer.validated_data['end_date']: + batch = models.Batch.objects.create( + batch_name=serializer.validated_data['batch_name'], + start_date=serializer.validated_data['start_date'], + end_date=serializer.validated_data['end_date'], + shipping_cost=serializer.validated_data['shipping_cost'], + ) + else: + raise rest_framework_exceptions.PermissionDenied(_( + 'Start Date must be earlier than End Date.' + )) + return response.Response( + {'id': batch.id}, + status=status.HTTP_201_CREATED + ) + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + if not self.request.user.is_staff: + return queryset.filter(user=self.request.user) + return queryset + + +class BatchDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Batch.objects.all() + serializer_class = api_serializers.BatchSerializer + + +class BatchCreate(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + serializer_class = api_serializers.BatchCreateSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + if serializer.validated_data['start_date'] < serializer.validated_date['end_date']: + batch = models.Batch.objects.create( + batch_name=serializer.validated_data['batch_name'], + start_date=serializer.validated_data['start_date'], + end_date=serializer.validated_data['end_date'], + shipping_cost=serializer.validated_data['shipping_cost'], + ) + else: + raise rest_framework_exceptions.PermissionDenied(_( + 'Start Date must be earlier than End Date.' + )) + return response.Response( + {'id': batch.id}, + status=status.HTTP_201_CREATED + ) diff --git a/api/views/cart.py b/api/views/cart.py new file mode 100644 index 0000000000000000000000000000000000000000..708b7ae87738e24fe5050536bb16f0af845203e4 --- /dev/null +++ b/api/views/cart.py @@ -0,0 +1,256 @@ +from api import models, paginations +from api import permissions as api_permissions +from api import serializers as api_serializers +from api import utils as api_utils +from django import shortcuts +from django.db import transaction as db_transaction +from django.db import utils as db_utils +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django_filters import rest_framework +from home_industry import utils as home_industry_utils +from rest_framework import exceptions as rest_framework_exceptions +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions +from rest_framework import response, status +from rest_framework import views as rest_framework_views + + +class CartUpdate(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + serializer_class = api_serializers.CartUpdateSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = request.user + product = shortcuts.get_object_or_404( + models.Product, + id=serializer.validated_data['product'] + ) + shopping_cart = models.ShoppingCart.objects.get(user=user) + cart_item, _created = models.CartItem.objects.get_or_create( + product=product, + shopping_cart=shopping_cart + ) + validated_hampers_messages = '' + try: + validated_hampers_messages = serializer.validated_data['hampers_messages'] + except KeyError: + pass + if serializer.validated_data['quantity'] == 0: + cart_item.delete() + else: + cart_item.hampers_messages = validated_hampers_messages + cart_item.quantity = serializer.validated_data['quantity'] + cart_item.save() + return response.Response(status=status.HTTP_204_NO_CONTENT) + + +class CartOverview(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + + def get(self, request, _format=None): + user = request.user + shopping_cart = models.ShoppingCart.objects.get(user=user) + item_subtotal = sum( + (cart_item.product.price + cart_item.product.hampers_price) + * cart_item.quantity for cart_item in shopping_cart.cart_items.all() + ) + shipping_costs = api_utils.get_shipping_costs(user) + if shipping_costs is None: + return response.Response( + {'item_subtotal': str(item_subtotal)}, + status=status.HTTP_200_OK + ) + return response.Response( + {'item_subtotal': str(item_subtotal), + 'shipping_costs': str(shipping_costs)}, + status=status.HTTP_200_OK + ) + + +class CartCheckout(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + serializer_class = api_serializers.CartCheckoutSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = request.user + shipment_config = home_industry_utils.get_shipment_config() + if user.sub_district.lower() != shipment_config.sub_district.lower(): + raise rest_framework_exceptions.ParseError(_( # pylint: disable=no-member + 'Cannot process shipment to other sub-districts other than {sub_district}.' + ).format(sub_district=shipment_config.sub_district)) + shopping_cart = models.ShoppingCart.objects.get(user=user) + cart_items = shopping_cart.cart_items.all() + if not cart_items.exists(): + raise rest_framework_exceptions.ParseError(_( + 'Unable to checkout because there are no items purchased.' + )) + api_utils.validate_product_stock(cart_items) + transaction_status = ( + '001' if serializer.validated_data['payment_method'] == 'TRF' else '002' + ) + + transaction = models.Transaction.objects.create( + user=user, + payment_method=serializer.validated_data['payment_method'], + donation=serializer.validated_data['donation'], + transaction_status=transaction_status, + batch=( + None if serializer.validated_data['payment_method'] == 'TRF' + else models.Batch.objects.filter( + start_date__lte=timezone.now().date(), + end_date__gte=timezone.now().date()).first() + ) + ) + + is_success = True + for cart_item in cart_items: + product = cart_item.product + if product.stock is not None: + try: + with db_transaction.atomic(): + product.stock -= cart_item.quantity + profit = cart_item.quantity*product.profit + product.save() + except db_utils.IntegrityError: + is_success = False + models.TransactionItem.objects.create( + transaction=transaction, + product=product, + quantity=cart_item.quantity, + profit=profit, + hampers_price=product.hampers_price, + hampers_messages=cart_item.hampers_messages + ) + cart_item.delete() + if not is_success: + transaction.transaction_status = '007' + transaction.save() + raise rest_framework_exceptions.APIException(_('Checkout failed.')) + return response.Response({'transaction': transaction.id}, status=status.HTTP_200_OK) + + +class CartUploadPOP(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + serializer_class = api_serializers.CartUploadPOPSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = request.user + bank_account_transfer_destination = shortcuts.get_object_or_404( + models.BankAccountTransferDestination, + id=serializer.validated_data['bank_account_transfer_destination'] + ) + transaction = shortcuts.get_object_or_404( + models.Transaction, + id=serializer.validated_data['transaction'], + user=user + ) + if transaction.payment_method != 'TRF': + raise rest_framework_exceptions.PermissionDenied(_( + 'The payment method for this transaction is not a transfer.' + )) + if transaction.transaction_status not in ('001', '002'): + raise rest_framework_exceptions.PermissionDenied(_( + 'Cannot upload proof of payment at this stage.' + )) + transaction.proof_of_payment = serializer.validated_data['proof_of_payment'] + transaction.user_bank_name = serializer.validated_data['user_bank_name'] + transaction.user_bank_account_name = serializer.validated_data['user_bank_account_name'] + transaction.bank_account_transfer_destination = bank_account_transfer_destination + transaction.update_bank_account_transfer_destination = True + transaction.transaction_status = '002' + transaction.batch = models.Batch.objects.filter( + start_date__lte=timezone.now().date(), + end_date__gte=timezone.now().date()).first() + transaction.save() + return response.Response(status=status.HTTP_204_NO_CONTENT) + + +class CartCompleteTransaction(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + serializer_class = api_serializers.CartCompleteOrCancelTransactionSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + transaction = shortcuts.get_object_or_404( + models.Transaction, + id=serializer.validated_data['transaction'], + user=request.user + ) + if transaction.transaction_status != '004': + raise rest_framework_exceptions.PermissionDenied(_( + 'Transaction cannot be completed unless the status is "Being shipped".' + )) + transaction.transaction_status = '005' + transaction.save() + return response.Response(status=status.HTTP_204_NO_CONTENT) + + +class CartCancelTransaction(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + serializer_class = api_serializers.CartCompleteOrCancelTransactionSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = request.user + transaction = shortcuts.get_object_or_404( + models.Transaction, + id=serializer.validated_data['transaction'], + user=user + ) + if transaction.transaction_status not in ('001', '002'): + raise rest_framework_exceptions.PermissionDenied(_( + 'Transaction cannot be canceled at this stage.' + )) + transaction_items = transaction.transaction_items.all() + api_utils.return_transaction_items_to_product_stock(transaction_items) + transaction.transaction_status = '006' + transaction.save() + return response.Response(status=status.HTTP_204_NO_CONTENT) + + +class ShoppingCartList(generics.ListAPIView): + filter_backends = [rest_framework.DjangoFilterBackend] + filterset_fields = ['user'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [rest_framework_permissions.IsAdminUser] + queryset = models.ShoppingCart.objects.all() + serializer_class = api_serializers.ShoppingCartSerializer + + +class ShoppingCartDetail(generics.RetrieveAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrOwner, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.ShoppingCart.objects.all() + serializer_class = api_serializers.ShoppingCartSerializer + + def get_object(self): + if self.kwargs.get('pk') == 'self': + self.kwargs['pk'] = models.ShoppingCart.objects.get( + user=self.request.user).id + return super().get_object() diff --git a/api/views/choices.py b/api/views/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..53dbc86e48b36d129d27384bcb3f808ae7627ef6 --- /dev/null +++ b/api/views/choices.py @@ -0,0 +1,29 @@ +from api import constants +from api import utils as api_utils +from rest_framework import permissions as rest_framework_permissions +from rest_framework import response, status +from rest_framework import views as rest_framework_views + + +class ChoicesAPIView(rest_framework_views.APIView): + choices = None + permission_classes = [rest_framework_permissions.IsAuthenticated] + + def get(self, request, _format=None): + assert self.choices is not None, ( + '{} should include a `choices` attribute.'.format( + self.__class__.__name__) + ) + return response.Response(api_utils.map_choices(self.choices), status=status.HTTP_200_OK) + + +class PaymentMethodChoices(ChoicesAPIView): + choices = constants.PAYMENT_METHOD_CHOICES + + +class TransactionStatusChoices(ChoicesAPIView): + choices = constants.TRANSACTION_STATUS_CHOICES + + +class DonationStatusChoices(ChoicesAPIView): + choices = constants.DONATION_STATUS_CHOICES diff --git a/api/views/config.py b/api/views/config.py new file mode 100644 index 0000000000000000000000000000000000000000..4bc731bcf72130d0a28ebe551515dd3e4f4ae7c2 --- /dev/null +++ b/api/views/config.py @@ -0,0 +1,36 @@ +from api import models +from api import permissions as api_permissions +from api import serializers as api_serializers +from django import shortcuts +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions + + +class AppConfigDetail(generics.RetrieveUpdateAPIView): + permission_classes = [rest_framework_permissions.IsAdminUser] + queryset = models.AppConfig.objects.all() + serializer_class = api_serializers.AppConfigSerializer + + def get_object(self): + obj = shortcuts.get_object_or_404(models.AppConfig) + return obj + + +class HelpContactConfigDetail(generics.RetrieveUpdateAPIView): + permission_classes = [api_permissions.IsAdminUserOrReadOnly] + queryset = models.HelpContactConfig.objects.all() + serializer_class = api_serializers.HelpContactConfigSerializer + + def get_object(self): + obj = shortcuts.get_object_or_404(models.HelpContactConfig) + return obj + + +class ShipmentConfigDetail(generics.RetrieveUpdateAPIView): + permission_classes = [api_permissions.IsAdminUserOrReadOnly] + queryset = models.ShipmentConfig.objects.all() + serializer_class = api_serializers.ShipmentConfigSerializer + + def get_object(self): + obj = shortcuts.get_object_or_404(models.ShipmentConfig) + return obj diff --git a/api/views/donation.py b/api/views/donation.py new file mode 100644 index 0000000000000000000000000000000000000000..ce8f89968490a6b4a83f2a93915c461763abfc8a --- /dev/null +++ b/api/views/donation.py @@ -0,0 +1,115 @@ +from api import models +from api import serializers as api_serializers +from django import shortcuts +from django.utils.translation import gettext_lazy as _ +from rest_framework import exceptions as rest_framework_exceptions +from rest_framework import permissions as rest_framework_permissions +from rest_framework import response, status +from rest_framework import views as rest_framework_views + + +class DonationCreate(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + serializer_class = api_serializers.DonationCreateSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = request.user + program = shortcuts.get_object_or_404( + models.Program, + id=serializer.validated_data['program'] + ) + if not program.open_donation: + raise rest_framework_exceptions.PermissionDenied(_( + 'This program is currently not accepting donations.' + )) + program_donation = None + if serializer.validated_data['donation_type'] == 'CSH': + bank_account_transfer_destination = shortcuts.get_object_or_404( + models.BankAccountTransferDestination, + id=serializer.validated_data['bank_account_transfer_destination'] + ) + program_donation = models.ProgramDonation.objects.create( + user=user, + program=program, + donation_type='CSH', + amount=serializer.validated_data['amount'], + proof_of_bank_transfer=serializer.validated_data['proof_of_bank_transfer'], + user_bank_name=serializer.validated_data['user_bank_name'], + user_bank_account_name=serializer.validated_data['user_bank_account_name'], + bank_account_transfer_destination=bank_account_transfer_destination + ) + + else: + if serializer.validated_data['delivery_method'] == 'DLV': + program_donation = models.ProgramDonation.objects.create( + user=user, + program=program, + donation_type='GDS', + goods_quantity=serializer.validated_data['goods_quantity'], + goods_description=serializer.validated_data['goods_description'], + delivery_method=serializer.validated_data['delivery_method'], + delivery_address=None + ) + else: + program_donation = models.ProgramDonation.objects.create( + user=user, + program=program, + donation_type='GDS', + goods_quantity=serializer.validated_data['goods_quantity'], + goods_description=serializer.validated_data['goods_description'], + delivery_method=serializer.validated_data['delivery_method'], + delivery_address=serializer.validated_data['delivery_address'] + ) + + return response.Response( + {'program_donation': program_donation.id}, + status=status.HTTP_200_OK + ) + + +class DonationReuploadProofOfBankTransfer(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAuthenticated] + serializer_class = api_serializers.DonationReuploadProofOfBankTransferSerializer + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = request.user + bank_account_transfer_destination = shortcuts.get_object_or_404( + models.BankAccountTransferDestination, + id=serializer.validated_data['bank_account_transfer_destination'] + ) + program_donation = shortcuts.get_object_or_404( + models.ProgramDonation, + id=serializer.validated_data['program_donation'], + user=user + ) + if program_donation.donation_status not in ('001', '004'): + raise rest_framework_exceptions.PermissionDenied(_( + 'Cannot reupload proof of bank transfer at this stage.' + )) + if program_donation.donation_type != 'CSH': + raise rest_framework_exceptions.PermissionDenied(_( + 'Cannot proof of bank transfer foor good donation.' + )) + program_donation.amount = serializer.validated_data['amount'] + program_donation.proof_of_bank_transfer = ( + serializer.validated_data['proof_of_bank_transfer'] + ) + program_donation.user_bank_name = serializer.validated_data['user_bank_name'] + program_donation.user_bank_account_name = ( + serializer.validated_data['user_bank_account_name'] + ) + program_donation.bank_account_transfer_destination = bank_account_transfer_destination + program_donation.update_bank_account_transfer_destination = True + program_donation.donation_status = '001' + program_donation.save() + return response.Response(status=status.HTTP_204_NO_CONTENT) diff --git a/api/views/product.py b/api/views/product.py new file mode 100644 index 0000000000000000000000000000000000000000..aa4bdad8b4a5bb6de927ecc3a1b1b2bf8ac5655b --- /dev/null +++ b/api/views/product.py @@ -0,0 +1,216 @@ +from api import exceptions as api_exceptions +from api import models, paginations +from api import permissions as api_permissions +from api import serializers as api_serializers +from django.db.models import deletion +from django.utils.translation import gettext_lazy as _ +from django_filters import rest_framework +from rest_framework import filters as rest_framework_filters +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions +from rest_framework import response, status + + +class CategoryList(generics.ListCreateAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_fields = ['name'] + ordering_fields = ['name'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Category.objects.all() + search_fields = ['name'] + serializer_class = api_serializers.CategorySerializer + + +class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Category.objects.all() + serializer_class = api_serializers.CategorySerializer + + def destroy(self, request, *_args, **_kwargs): + instance = self.get_object() + try: + instance.delete() + except deletion.ProtectedError: + raise api_exceptions.IntegrityError(_( + 'Cannot delete category due to integrity error.' + )) + return response.Response(status=status.HTTP_204_NO_CONTENT) + + +class SubcategoryList(generics.ListCreateAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_fields = ['name', 'category'] + ordering_fields = ['name'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Subcategory.objects.all() + search_fields = ['name'] + serializer_class = api_serializers.SubcategorySerializer + + +class SubcategoryDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Subcategory.objects.all() + serializer_class = api_serializers.SubcategorySerializer + + def destroy(self, request, *_args, **_kwargs): + instance = self.get_object() + try: + instance.delete() + except deletion.ProtectedError: + raise api_exceptions.IntegrityError(_( + 'Cannot delete subcategory due to integrity error.' + )) + return response.Response(status=status.HTTP_204_NO_CONTENT) + + +class ProductList(generics.ListCreateAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_fields = ['code', 'subcategory', 'subcategory__category'] + ordering_fields = ['name', 'price', 'stock'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Product.objects.all() + search_fields = ['code', 'name'] + serializer_class = api_serializers.ProductSerializer + + def post(self, request, _format=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + validated_image = None + validated_is_hampers = False + validated_hampers_price = 0 + try: + validated_image = serializer.validated_data['image'] + validated_is_hampers=serializer.validated_data['is_hampers'] + validated_hampers_price = serializer.validated_data['hampers_price'] + except KeyError: + pass + try: + validated_is_hampers=serializer.validated_data['is_hampers'] + except KeyError: + pass + try: + validated_hampers_price = serializer.validated_data['hampers_price'] + except KeyError: + pass + if serializer.validated_data['preorder']: + product = models.Product.objects.create( + name=serializer.validated_data['name'], + description=serializer.validated_data['description'], + price=serializer.validated_data['price'], + modal=serializer.validated_data['modal'], + subcategory=models.Subcategory.objects.get(name=serializer.validated_data['subcategory']), + total_profit=0, + unit=serializer.validated_data['unit'], + is_hampers=validated_is_hampers, + hampers_price=validated_hampers_price, + image=validated_image, + preorder=serializer.validated_data['preorder'], + preorder_duration=serializer.validated_data['preorder_duration'], + ) + else: + product = models.Product.objects.create( + name=serializer.validated_data['name'], + description=serializer.validated_data['description'], + price=serializer.validated_data['price'], + stock=serializer.validated_data['stock'], + modal=serializer.validated_data['modal'], + subcategory=models.Subcategory.objects.get(name=serializer.validated_data['subcategory']), + total_profit=0, + unit=serializer.validated_data['unit'], + is_hampers=validated_is_hampers, + hampers_price=validated_hampers_price, + image=validated_image, + ) + product.profit = (product.price - product.modal) + product.save() + return response.Response( + {'id': product.id}, + status=status.HTTP_201_CREATED + ) + + +class ProductDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Product.objects.all() + serializer_class = api_serializers.ProductSerializer + + def get_serializer(self, *args, **kwargs): + # leave this intact + serializer_class = self.get_serializer_class() + kwargs["context"] = self.get_serializer_context() + + if (self.request != None): + if (self.request.data.get("price") != None) and (self.request.data.get("modal") != None): + draft_request_data = self.request.data.copy() + profit = int(self.request.data['price']) - \ + int(self.request.data['modal']) + new_profit = {'profit': profit} + draft_request_data.update(new_profit) + kwargs["data"] = draft_request_data + return serializer_class(*args, **kwargs) + else: + if (self.request.data.get("price") != None): + + instance = self.get_object() + draft_request_data = self.request.data.copy() + profit = int( + self.request.data['price'])-int(instance.modal) + new_profit = {'profit': profit} + draft_request_data.update(new_profit) + + kwargs["data"] = draft_request_data + return serializer_class(*args, **kwargs) + elif (self.request.data.get("modal") != None): + instance = self.get_object() + draft_request_data = self.request.data.copy() + profit = int(instance.price) - \ + int(self.request.data['modal']) + new_profit = {'profit': profit} + draft_request_data.update(new_profit) + + kwargs["data"] = draft_request_data + return serializer_class(*args, **kwargs) + + return serializer_class(*args, **kwargs) + + def partial_update(self, request, *args, **kwargs): + instance = self.get_object() + + serializer = self.get_serializer( + instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + + return super().partial_update(request, *args, **kwargs) # pylint: disable=no-member diff --git a/api/views/program.py b/api/views/program.py new file mode 100644 index 0000000000000000000000000000000000000000..0784a549d8db05435b67628770faf3dad56b9c44 --- /dev/null +++ b/api/views/program.py @@ -0,0 +1,164 @@ +from api import filters as api_filters +from api import models, paginations +from api import permissions as api_permissions +from api import schemas +from api import serializers as api_serializers +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.csrf import csrf_exempt +from django_filters import rest_framework +from rest_framework import exceptions as rest_framework_exceptions +from rest_framework import filters as rest_framework_filters +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions +from rest_framework import response, status +from rest_framework.decorators import api_view + + +class ProgramProgressList(generics.ListCreateAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + lookup_field = 'program' + serializer_class = api_serializers.ProgramProgressSerializer + + def get_queryset(self): + return models.ProgramProgress.objects.filter(program=self.get_program()) + + def perform_create(self, serializer): + serializer.save(program=self.get_program()) + + def get_program(self): + program = models.Program.objects.filter( + id=self.kwargs.get(self.lookup_field)).first() + if program is None: + raise rest_framework_exceptions.NotFound(_( + 'Program not found.' + )) + return program + + +class ProgramList(generics.ListCreateAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_fields = ['code'] + ordering_fields = ['name', 'start_date_time', 'end_date_time'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Program.objects.all() + search_fields = ['code', 'name'] + serializer_class = api_serializers.ProgramSerializer + + +class ProgramDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Program.objects.all() + serializer_class = api_serializers.ProgramSerializer + + +class ProgramDonationList(generics.ListAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_class = api_filters.ProgramDonationFilter + ordering_fields = ['created_at', 'updated_at'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [rest_framework_permissions.IsAuthenticated] + queryset = models.ProgramDonation.objects.all() + schema = schemas.ProgramDonationListSchema() + search_fields = ['donation_number', 'user_full_name', + 'program_name', 'donation_type'] + serializer_class = api_serializers.ProgramDonationSerializer + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + if not self.request.user.is_staff: + return queryset.filter(user=self.request.user) + return queryset + + +@api_view(['DELETE']) +@csrf_exempt +def delete_donation_by_program(request, pid): + if ((request.user) and (request.user.is_staff)): + program = models.Program.objects.get(id=pid) + donation = models.ProgramDonation.objects.filter(program=program) + donation.delete() + return response.Response(status=status.HTTP_204_NO_CONTENT) + + +class ProgramDonationListCSH(generics.ListAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_class = api_filters.ProgramDonationFilter + ordering_fields = ['created_at', 'updated_at'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [rest_framework_permissions.IsAuthenticated] + queryset = models.ProgramDonation.objects.filter(donation_type='CSH') + schema = schemas.ProgramDonationListSchema() + search_fields = ['donation_number', 'user_full_name', + 'program_name', 'donation_type'] + serializer_class = api_serializers.ProgramDonationSerializer + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + if not self.request.user.is_staff: + return queryset.filter(user=self.request.user) + return queryset + + +class ProgramDonationListGDS(generics.ListAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_class = api_filters.ProgramDonationFilter + ordering_fields = ['created_at', 'updated_at'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [rest_framework_permissions.IsAuthenticated] + queryset = models.ProgramDonation.objects.filter(donation_type='GDS') + schema = schemas.ProgramDonationListSchema() + search_fields = ['donation_number', 'user_full_name', + 'program_name', 'donation_type'] + serializer_class = api_serializers.ProgramDonationSerializer + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + # if not self.request.user.is_staff: + # return queryset.filter(user=self.request.user) + # return queryset + return queryset.filter(user=self.request.user) + + +class ProgramDonationDetail(generics.RetrieveUpdateAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrOwnerReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.ProgramDonation.objects.all() + serializer_class = api_serializers.ProgramDonationSerializer + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + if instance.donation_status in ('002', '003'): + raise rest_framework_exceptions.PermissionDenied(_( + 'Cannot update program donation because it has a completed or canceled status.' + )) + return super().update(request, *args, **kwargs) # pylint: disable=no-member diff --git a/api/views/report.py b/api/views/report.py new file mode 100644 index 0000000000000000000000000000000000000000..fdf21154c1107cfbc2341ff141b58a223124e775 --- /dev/null +++ b/api/views/report.py @@ -0,0 +1,83 @@ +from api import reports_writer, schemas +from api import serializers as api_serializers +from django import http +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from rest_framework import permissions as rest_framework_permissions +from rest_framework import views as rest_framework_views + + +class ReportAPIView(rest_framework_views.APIView): + permission_classes = [rest_framework_permissions.IsAdminUser] + report_function = None + serializer_class = None + + def get_filename(self, query_params): + raise NotImplementedError + + def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) # pylint: disable=not-callable + + def get(self, request, _format=None): + assert self.report_function is not None, ( + '{} should include a `report_function` attribute.'.format( + self.__class__.__name__) + ) + assert self.report_function is not None, ( + '{} should include a `serializer_class` attribute.'.format( + self.__class__.__name__) + ) + serializer = self.get_serializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + buffer = self.report_function.__func__(serializer.validated_data) + buffer.seek(0) + return http.FileResponse(buffer, filename=self.get_filename(serializer.validated_data)) + + +class ReportTransaction(ReportAPIView): + report_function = reports_writer.create_transaction_report + schema = schemas.ReportTransactionSchema() + serializer_class = api_serializers.ReportTransactionSerializer + + def get_filename(self, query_params): + filename = '{}.xlsx'.format(_( # pylint: disable=no-member + 'Transaction Report from {date_from} to {date_to} for {batch_name}' + ).format( + date_from=query_params.get('created_at_date_range_after', '_'), + date_to=query_params.get( + 'created_at_date_range_before', str(timezone.now())[:10]), + batch_name=query_params.get('batch_name', '_') + )) + return filename + + +class ReportProgramDonationCSH(ReportAPIView): + report_function = reports_writer.create_program_donation_report_csh + schema = schemas.ReportProgramDonationSchema() + serializer_class = api_serializers.ReportProgramDonationSerializer + + def get_filename(self, query_params): + filename = '{}.xlsx'.format(_( # pylint: disable=no-member + 'Program Donation Report from {date_from} to {date_to}' + ).format( + date_from=query_params.get('created_at_date_range_after', '_'), + date_to=query_params.get( + 'created_at_date_range_before', str(timezone.now())[:10]) + )) + return filename + + +class ReportProgramDonationGDS(ReportAPIView): + report_function = reports_writer.create_program_donation_report_gds + schema = schemas.ReportProgramDonationSchema() + serializer_class = api_serializers.ReportProgramDonationSerializer + + def get_filename(self, query_params): + filename = '{}.xlsx'.format(_( # pylint: disable=no-member + 'Program Donation Report from {date_from} to {date_to}' + ).format( + date_from=query_params.get('created_at_date_range_after', '_'), + date_to=query_params.get( + 'created_at_date_range_before', str(timezone.now())[:10]) + )) + return filename diff --git a/api/views/transaction.py b/api/views/transaction.py new file mode 100644 index 0000000000000000000000000000000000000000..59a05feef7c826e00f35496b483fd820329721d3 --- /dev/null +++ b/api/views/transaction.py @@ -0,0 +1,65 @@ +from api import filters as api_filters +from api import models, paginations +from api import permissions as api_permissions +from api import schemas +from api import serializers as api_serializers +from api import utils as api_utils +from django.utils.translation import gettext_lazy as _ +from django_filters import rest_framework +from rest_framework import exceptions as rest_framework_exceptions +from rest_framework import filters as rest_framework_filters +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions + + +class TransactionList(generics.ListAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_class = api_filters.TransactionFilter + ordering_fields = ['created_at', 'updated_at'] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [rest_framework_permissions.IsAuthenticated] + queryset = models.Transaction.objects.all() + schema = schemas.TransactionListSchema() + search_fields = ['transaction_number', 'user_full_name'] + serializer_class = api_serializers.TransactionSerializer + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + if not self.request.user.is_staff: + return queryset.filter(user=self.request.user) + return queryset + + +class TransactionDetail(generics.RetrieveUpdateAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrOwnerReadOnly, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.Transaction.objects.all() + serializer_class = api_serializers.TransactionSerializer + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + if instance.transaction_status in ('005', '006', '007'): + raise rest_framework_exceptions.PermissionDenied(_( + 'Cannot update transaction because it has a completed, canceled, or failed status.' + )) + if serializer.validated_data['transaction_status'] == '006': + transaction_items = instance.transaction_items.all() + api_utils.return_transaction_items_to_product_stock( + transaction_items) + + if serializer.validated_data['transaction_status'] == '005': + transaction_items = instance.transaction_items.all() + for transaction_item in transaction_items: + product = transaction_item.product + product.total_profit += transaction_item.profit + product.save() + + return super().update(request, *args, **kwargs) # pylint: disable=no-member diff --git a/api/views/user.py b/api/views/user.py new file mode 100644 index 0000000000000000000000000000000000000000..e27b2171025f7b0a7a99383c26707fccd4208873 --- /dev/null +++ b/api/views/user.py @@ -0,0 +1,39 @@ +from api import models, paginations +from api import permissions as api_permissions +from api import serializers as api_serializers +from django_filters import rest_framework +from rest_framework import filters as rest_framework_filters +from rest_framework import generics +from rest_framework import permissions as rest_framework_permissions + + +class UserList(generics.ListCreateAPIView): + filter_backends = [ + rest_framework.DjangoFilterBackend, + rest_framework_filters.OrderingFilter, + rest_framework_filters.SearchFilter, + ] + filterset_fields = ['username', 'phone_number'] + ordering_fields = [ + 'username', 'full_name', 'phone_number', + 'total_transactions', 'total_program_donations_goods', 'total_program_donations_cash' + ] + pagination_class = paginations.SmallResultsSetPagination + permission_classes = [rest_framework_permissions.IsAdminUser] + queryset = models.User.objects.all() + search_fields = ['username', 'full_name', 'phone_number'] + serializer_class = api_serializers.UserSerializer + + +class UserDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [ + api_permissions.IsAdminUserOrSelf, + rest_framework_permissions.IsAuthenticated, + ] + queryset = models.User.objects.all() + serializer_class = api_serializers.UserSerializer + + def get_object(self): + if self.kwargs.get('pk') == 'self': + self.kwargs['pk'] = self.request.user.id + return super().get_object() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a9ad435a5a0a84d7d67c4b85d39b84c12372a3b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: "3.9" + +services: + db: + image: postgres:13 + environment: + - POSTGRES_DB=home_industry + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_HOST=127.0.0.1 + web: + build: . + command: bash -c " + python manage.py makemigrations && + python manage.py migrate && + /wait-for-it.sh db:5432 -- python manage.py runserver 0.0.0.0:8000" + volumes: + - ./wait-for-it.sh:/wait-for-it.sh + - .:/code + ports: + - "8000:8000" + depends_on: + - db + env_file: .env \ No newline at end of file diff --git a/home_industry/settings/ci.py b/home_industry/settings/ci.py index 57034ee49c2ef9ce1f75f89744c172d7bc07c155..a8952d69592400efe0f1cb2d07fa7bc6ba900efd 100644 --- a/home_industry/settings/ci.py +++ b/home_industry/settings/ci.py @@ -149,6 +149,11 @@ HOME_INDUSTRY_ADMIN_SITE = { CORS_ORIGIN_ALLOW_ALL = True +# HTTP headers that are to be exposed to the browser +CORS_EXPOSE_HEADERS = [ + 'Content-Disposition', +] + # django-rest-knox # https://github.com/James1345/django-rest-knox diff --git a/home_industry/settings/development.py b/home_industry/settings/development.py index bdd9ba2f39f41c1ffe980d41aac94e4d870cb351..5c99f74882ba81eab7add80c4c58e01d44b92c83 100644 --- a/home_industry/settings/development.py +++ b/home_industry/settings/development.py @@ -153,6 +153,11 @@ HOME_INDUSTRY_ADMIN_SITE = { CORS_ORIGIN_ALLOW_ALL = True +# HTTP headers that are to be exposed to the browser +CORS_EXPOSE_HEADERS = [ + 'Content-Disposition', +] + # django-rest-knox # https://github.com/James1345/django-rest-knox diff --git a/home_industry/settings/local.py b/home_industry/settings/local.py index bdd9ba2f39f41c1ffe980d41aac94e4d870cb351..5c99f74882ba81eab7add80c4c58e01d44b92c83 100644 --- a/home_industry/settings/local.py +++ b/home_industry/settings/local.py @@ -153,6 +153,11 @@ HOME_INDUSTRY_ADMIN_SITE = { CORS_ORIGIN_ALLOW_ALL = True +# HTTP headers that are to be exposed to the browser +CORS_EXPOSE_HEADERS = [ + 'Content-Disposition', +] + # django-rest-knox # https://github.com/James1345/django-rest-knox diff --git a/home_industry/settings/production.py b/home_industry/settings/production.py index bdd9ba2f39f41c1ffe980d41aac94e4d870cb351..5c99f74882ba81eab7add80c4c58e01d44b92c83 100644 --- a/home_industry/settings/production.py +++ b/home_industry/settings/production.py @@ -153,6 +153,11 @@ HOME_INDUSTRY_ADMIN_SITE = { CORS_ORIGIN_ALLOW_ALL = True +# HTTP headers that are to be exposed to the browser +CORS_EXPOSE_HEADERS = [ + 'Content-Disposition', +] + # django-rest-knox # https://github.com/James1345/django-rest-knox diff --git a/home_industry/settings/staging.py b/home_industry/settings/staging.py index bdd9ba2f39f41c1ffe980d41aac94e4d870cb351..5c99f74882ba81eab7add80c4c58e01d44b92c83 100644 --- a/home_industry/settings/staging.py +++ b/home_industry/settings/staging.py @@ -153,6 +153,11 @@ HOME_INDUSTRY_ADMIN_SITE = { CORS_ORIGIN_ALLOW_ALL = True +# HTTP headers that are to be exposed to the browser +CORS_EXPOSE_HEADERS = [ + 'Content-Disposition', +] + # django-rest-knox # https://github.com/James1345/django-rest-knox diff --git a/sonar-project.properties b/sonar-project.properties index 43bcb24b79aabba2bc8e10595b72ded1de2cb946..96939107c8056dda55e5dff5b37921352e460c3d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,5 @@ +sonar.projectKey=ppl2021-a-pilar_be +sonar.branch.name=dev sonar.exclusions=**/schemas.py,**/seeds.py,**signals.py,**/tests.py,**/migrations/** sonar.python.coverage.reportPaths=coverage.xml sonar.scm.provider=git diff --git a/wait-for-it.sh b/wait-for-it.sh new file mode 100755 index 0000000000000000000000000000000000000000..8bb4cf008b0f9036963bcbe732bb07a8f3f99fbc --- /dev/null +++ b/wait-for-it.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi +'+