diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5c98b428844d9f7d529e2b6fb918d15bf072f3df --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/backend.iml b/.idea/backend.iml new file mode 100644 index 0000000000000000000000000000000000000000..d6ebd4805981b8400db3e3291c74a743fef9a824 --- /dev/null +++ b/.idea/backend.iml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000000000000000000000000000000000..a55e7a179bde3e4e772c29c0c85e53354aa54618 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> + </state> +</component> \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000000000000000000000000000000000000..15a15b218a29e09c9190992732698d646e4d659a --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Encoding" addBOMForNewFiles="with NO BOM" /> +</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..28a804d8932aba40f168fd757a74cb718a955a1a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="JavaScriptSettings"> + <option name="languageLevel" value="ES6" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..e066844ef633dcf7d83f24067bbb6305f517da56 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/backend.iml" filepath="$PROJECT_DIR$/.idea/backend.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..7a9dfa044d022f5ca8cf9191e37fef926dd67f77 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..daa228380b867596167ad844651d5aa9a9c49f24 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "env/bin/python3" +} \ No newline at end of file diff --git a/api/constants.py b/api/constants.py index 38b00b712b6dcfbdb68b30d6b974d29281f96cb7..17d1bf71012b3f059c405f12494896ac267a39d9 100644 --- a/api/constants.py +++ b/api/constants.py @@ -7,6 +7,16 @@ DONATION_STATUS_CHOICES = [ ('004', _('Waiting for reupload of proof of bank transfer')), ] +DONATION_TYPE_CHOICES = [ + ('CSH', _('Cash')), + ('GDS', _('Goods')), +] + +DONATION_GOODS_DELIVERY_METHOD_CHOICES = [ + ('PCK', _('Pick Up')), + ('DLV', _('Delivered')), +] + PAYMENT_METHOD_CHOICES = [ ('TRF', _('Transfer')), ('COD', _('Cash on delivery')), diff --git a/api/filters.py b/api/filters.py index 67bfb5db05289d0de64d38d4f20a2460df2ff1a5..404d38684af595ef8d2dedce3c2dafa96fedfc08 100644 --- a/api/filters.py +++ b/api/filters.py @@ -29,6 +29,8 @@ class TransactionFilter(django_filters.FilterSet): 'payment_method', 'transaction_status', 'updated_at_date_range', + 'batch_name', + 'end_date' ] model = models.Transaction @@ -44,3 +46,10 @@ class ProgramDonationFilter(django_filters.FilterSet): 'updated_at_date_range', ] model = models.ProgramDonation + +class BatchFilter(django_filters.FilterSet): + created_at_date_range = django_filters.DateFromToRangeFilter(field_name='created_at') + + class Meta: + fields = ['id', 'batch_name', 'start_date', 'end_date', 'shipping_cost'] + model = models.Batch diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 573f92fdd25ac45bf37675cc9e5aa95c83938902..4eaad886774517d3a170ad3e3278f16172d938c3 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.7 on 2020-06-06 19:07 +# Generated by Django 3.0.7 on 2020-10-22 09:08 import api.utils from decimal import Decimal @@ -85,6 +85,21 @@ class Migration(migrations.Migration): 'unique_together': {('bank_name', 'bank_account_number')}, }, ), + migrations.CreateModel( + name='Batch', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')), + ('batch_name', models.CharField(max_length=200, verbose_name='name')), + ('start_date', models.DateField(verbose_name='start date')), + ('end_date', models.DateField(verbose_name='end date')), + ('shipping_cost', models.IntegerField(blank=True, null=True, verbose_name='shipping cost')), + ], + options={ + 'verbose_name': 'batch', + 'verbose_name_plural': 'batches', + 'ordering': ['-start_date', '-end_date', 'batch_name', 'id'], + }, + ), migrations.CreateModel( name='Category', fields=[ @@ -189,7 +204,10 @@ class Migration(migrations.Migration): ('transfer_destination_bank_account_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='transfer destination bank account 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')), + ('end_date', models.DateField(null=True, verbose_name='end date')), ('bank_account_transfer_destination', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='api.BankAccountTransferDestination', verbose_name='bank account transfer destination')), + ('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transaction', to='api.Batch', verbose_name='batch')), ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to=settings.AUTH_USER_MODEL, verbose_name='user')), ], options={ @@ -249,17 +267,22 @@ class Migration(migrations.Migration): ('user_full_name', models.CharField(max_length=200, verbose_name='user full name')), ('user_phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region=None, verbose_name='user phone number')), ('program_name', models.CharField(max_length=200, verbose_name='program name')), - ('amount', models.DecimalField(decimal_places=2, max_digits=12, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='amount')), + ('amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0.01'))], verbose_name='amount')), ('donation_status', models.CharField(choices=[('001', 'Waiting for admin confirmation'), ('002', 'Completed'), ('003', 'Canceled'), ('004', 'Waiting for reupload of proof of bank transfer')], default='001', max_length=3, verbose_name='donation status')), - ('proof_of_bank_transfer', models.ImageField(upload_to=api.utils.get_upload_file_path, verbose_name='proof of bank transfer')), + ('donation_type', models.CharField(choices=[('CSH', 'Cash'), ('GDS', 'Goods')], default='CSH', max_length=3, verbose_name='donation type')), + ('proof_of_bank_transfer', models.ImageField(blank=True, null=True, upload_to=api.utils.get_upload_file_path, verbose_name='proof of bank transfer')), ('user_bank_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='user bank name')), - ('user_bank_account_name', models.CharField(max_length=200, verbose_name='user bank account name')), + ('user_bank_account_name', models.CharField(blank=True, max_length=200, null=True, verbose_name='user bank account name')), ('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')), + ('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')), + ('delivery_address', models.CharField(blank=True, max_length=200, null=True, verbose_name='goods delivery address')), ('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')), - ('bank_account_transfer_destination', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='program_donations', to='api.BankAccountTransferDestination', verbose_name='bank account transfer destination')), + ('bank_account_transfer_destination', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='program_donations', to='api.BankAccountTransferDestination', verbose_name='bank account transfer destination')), ('program', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='program_donations', to='api.Program', verbose_name='program')), ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='program_donations', to=settings.AUTH_USER_MODEL, verbose_name='user')), ], diff --git a/api/models.py b/api/models.py index c493722d016da2b543f5ca3181a4cf617fc88792..2f56883fd99e0ada0ba5f6108b74aee9c7ed7a17 100644 --- a/api/models.py +++ b/api/models.py @@ -13,6 +13,8 @@ from api import constants, utils class User(auth_models.AbstractUser): + REQUIRED_FIELDS=['phone_number'] + id = db_models.UUIDField(default=uuid.uuid4, primary_key=True, verbose_name=_('ID')) first_name = None last_name = None @@ -303,6 +305,22 @@ class Transaction(db_models.Model): ) updated_at = db_models.DateTimeField(auto_now=True, db_index=True, verbose_name=_('updated at')) + batch = db_models.ForeignKey( + 'api.Batch', + null=True, + blank=True, + on_delete=db_models.CASCADE, + related_name='transaction', + verbose_name=_('batch') + ) + + batch_name = db_models.CharField(max_length=200, verbose_name=_('batch name')) + + end_date = db_models.DateField( + null=True, + verbose_name=_('end date') + ) + class Meta: ordering = ['-updated_at', '-created_at', 'transaction_number', 'id'] verbose_name = _('transaction') @@ -425,6 +443,8 @@ class ProgramDonation(db_models.Model): user_phone_number = modelfields.PhoneNumberField(verbose_name=_('user phone number')) program_name = db_models.CharField(max_length=200, verbose_name=_('program name')) amount = db_models.DecimalField( + blank=True, + null=True, decimal_places=2, max_digits=12, validators=[validators.MinValueValidator(decimal.Decimal('0.01'))], @@ -436,22 +456,33 @@ class ProgramDonation(db_models.Model): max_length=3, verbose_name=_('donation status') ) + donation_type = db_models.CharField( + choices=constants.DONATION_TYPE_CHOICES, + default='CSH', + max_length=3, + verbose_name=_('donation type') + ) proof_of_bank_transfer = db_models.ImageField( + blank=True, + null=True, upload_to=utils.get_upload_file_path, verbose_name=_('proof of bank transfer') ) user_bank_name = db_models.CharField( blank=True, - max_length=100, null=True, + max_length=100, verbose_name=_('user bank name') ) user_bank_account_name = db_models.CharField( + blank=True, + null=True, max_length=200, verbose_name=_('user bank account name') ) bank_account_transfer_destination = db_models.ForeignKey( 'api.BankAccountTransferDestination', + blank=True, null=True, on_delete=db_models.SET_NULL, related_name='program_donations', @@ -459,22 +490,49 @@ class ProgramDonation(db_models.Model): ) transfer_destination_bank_name = db_models.CharField( blank=True, - max_length=100, null=True, + max_length=100, verbose_name=_('transfer destination bank name') ) transfer_destination_bank_account_name = db_models.CharField( blank=True, - max_length=200, null=True, + max_length=200, verbose_name=_('transfer destination bank account name') ) transfer_destination_bank_account_number = db_models.CharField( blank=True, - max_length=100, null=True, + max_length=100, verbose_name=_('transfer destination bank account number') ) + goods_quantity = db_models.DecimalField( + blank=True, + null=True, + decimal_places=0, + max_digits=12, + validators=[validators.MinValueValidator(decimal.Decimal('1'))], + verbose_name=_('goods quantity') + ) + goods_description = db_models.CharField( + blank=True, + null=True, + max_length=200, + verbose_name=_('goods description') + ) + delivery_method = db_models.CharField( + choices=constants.DONATION_GOODS_DELIVERY_METHOD_CHOICES, + blank=True, + null=True, + max_length=3, + verbose_name=_('goods delivery method') + ) + delivery_address = db_models.CharField( + blank=True, + null=True, + max_length=200, + verbose_name=_('goods delivery address') + ) created_at = db_models.DateTimeField( auto_now_add=True, db_index=True, @@ -559,3 +617,32 @@ class ShipmentConfig(solo_models.SingletonModel): def __str__(self): return 'Shipment Configuration' + + +class Batch(db_models.Model): + id = db_models.UUIDField(default=uuid.uuid4, primary_key=True, verbose_name=_('ID')) + + batch_name = db_models.CharField(max_length=200, verbose_name=_('name')) + + start_date = db_models.DateField( + blank=False, + verbose_name=_('start date') + ) + end_date = db_models.DateField( + blank=False, + verbose_name=_('end date') + ) + + shipping_cost = db_models.IntegerField( + blank=True, + verbose_name=_('shipping cost'), + null=True + ) + + class Meta: + ordering = ['-start_date', '-end_date', 'batch_name', 'id'] + verbose_name = _('batch') + verbose_name_plural = _('batches') + + def __str__(self): + return self.batch_name diff --git a/api/schemas.py b/api/schemas.py index 1fb99dcfc0d85734384b1f8de8ed638e3b09250a..e91a3cbf0def71d95e5f300daddd1ece481a3087 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -31,6 +31,17 @@ class AutoSchemaWithDateRange(schemas.AutoSchema): class ReportTransactionSchema(AutoSchemaWithDateRange): date_range_fields = ['created_at_date_range'] + def get_filter_fields(self, path, method): + filter_fields = super().get_filter_fields(path, method) + filter_fields += [ + coreapi.Field( + 'batch_name', + location='query', + required=False, + schema=coreschema.String() + ) + ] + return filter_fields class ReportProgramDonationSchema(AutoSchemaWithDateRange): @@ -43,3 +54,7 @@ class TransactionListSchema(AutoSchemaWithDateRange): class ProgramDonationListSchema(AutoSchemaWithDateRange): date_range_fields = ['updated_at_date_range'] + + +class BatchListSchema(AutoSchemaWithDateRange): + date_range_fields = ['updated_at_date_range'] \ No newline at end of file diff --git a/api/seeds.py b/api/seeds.py index aff4d0050aebbc0c93effef957f3e69a6c9821a9..ee45fadb8fab07d20d71fc503ed8d06ce35a436f 100644 --- a/api/seeds.py +++ b/api/seeds.py @@ -57,10 +57,24 @@ PROGRAM_DATA = { 'link': 'https://example.com', } -PROGRAM_DONATION_DATA = { +PROGRAM_DONATION_CASH_DATA = { 'amount': '1000', - 'user_bank_name': 'Dummy User Bank Name', - 'user_bank_account_name': 'Dummy User Bank Account Name', + 'user_bank_name': 'Dummy Bank Name', + 'user_bank_account_name': 'Dummy Bank Account Name', +} + +PROGRAM_DONATION_GOODS_DATA = { + 'goods_quantity': '10', + 'goods_description': 'Dummy Goods', + 'delivery_address': 'Dummy Address', + 'delivery_method': 'PCK', +} + +PROGRAM_DONATION_GOODS_DATA_DLV = { + 'goods_quantity': '10', + 'goods_description': 'Dummy Goods', + 'delivery_address': None, + 'delivery_method': 'DLV', } HELP_CONTACT_CONFIG_DATA = { @@ -75,3 +89,10 @@ SHIPMENT_CONFIG_DATA = { 'same_urban_village_different_hamlet_costs': '2000', 'same_sub_district_different_urban_village_costs': '3000', } + +BATCH_DATA = { + 'batch_name': 'Batch 1', + 'start_date': '2020-10-08', + 'end_date': '2020-10-15', + 'shipping_cost': '60000', +} \ No newline at end of file diff --git a/api/serializers.py b/api/serializers.py index 157f7aa4c7c65ec1e5e920f8843d699a19bc8804..290967f246fd5cdd26593fd5e6bfc2700e2cd1ac 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -58,23 +58,53 @@ class CartCompleteOrCancelTransactionSerializer(serializers.Serializer): # pylin class DonationCreateSerializer(serializers.Serializer): # pylint: disable=abstract-method program = serializers.UUIDField(label=_('Program')) + donation_type = serializers.ChoiceField( + choices=constants.DONATION_TYPE_CHOICES, + label=_('Donation type') + ) amount = serializers.DecimalField( decimal_places=2, label=_('Amount'), max_digits=12, - min_value=decimal.Decimal('0.01') + min_value=decimal.Decimal('0.01'), + required=False ) proof_of_bank_transfer = serializers.ImageField(label=_('Proof of bank transfer')) user_bank_name = serializers.CharField( label=_('User bank name'), - max_length=100 + max_length=100, + required=False ) user_bank_account_name = serializers.CharField( label=_('User bank account name'), - max_length=200 + max_length=200, + required=False ) bank_account_transfer_destination = serializers.UUIDField( - label=_('Bank account transfer destination') + label=_('Bank account transfer destination'), + required=False + ) + goods_quantity = serializers.DecimalField( + decimal_places=0, + label=_('Goods quantity'), + max_digits=12, + min_value=decimal.Decimal('1'), + required=False + ) + goods_description = serializers.CharField( + label=_('Goods description'), + max_length=200, + required=False + ) + delivery_method = serializers.ChoiceField( + choices=constants.DONATION_GOODS_DELIVERY_METHOD_CHOICES, + label=_('Goods delivery method'), + required=False + ) + delivery_address = serializers.CharField( + label=_('Goods delivery address'), + max_length=200, + required=False ) @@ -299,10 +329,11 @@ class TransactionSerializer(serializers.ModelSerializer): readable_transaction_status = serializers.SerializerMethodField( 'get_readable_transaction_status' ) + transaction_items = TransactionItemSerializer(many=True, read_only=True) transaction_item_subtotal = serializers.SerializerMethodField('get_transaction_item_subtotal') subtotal = serializers.SerializerMethodField('get_subtotal') - + class Meta: fields = [ 'id', @@ -334,6 +365,9 @@ class TransactionSerializer(serializers.ModelSerializer): 'transaction_items', 'transaction_item_subtotal', 'subtotal', + 'batch', + 'batch_name', + 'end_date' ] model = models.Transaction read_only_fields = [ @@ -359,6 +393,9 @@ class TransactionSerializer(serializers.ModelSerializer): 'transfer_destination_bank_account_number', 'created_at', 'updated_at', + 'batch', + 'batch_name', + 'end_date' ] def get_readable_payment_method(self, obj): # pylint: disable=no-self-use @@ -414,7 +451,8 @@ class ProgramSerializer(serializers.ModelSerializer): def get_total_donation_amount(self, obj): # pylint: disable=no-self-use total_donation_amount = sum( program_donation.amount for program_donation in obj.program_donations.filter( - donation_status='002' + donation_status='002', + donation_type='CSH' ) ) return str(total_donation_amount) @@ -450,8 +488,9 @@ class ProgramDonationSerializer(serializers.ModelSerializer): 'user_full_name', 'user_phone_number', 'program_name', - 'amount', + 'donation_type', 'donation_status', + 'amount', 'readable_donation_status', 'proof_of_bank_transfer', 'user_bank_name', @@ -460,6 +499,10 @@ class ProgramDonationSerializer(serializers.ModelSerializer): 'transfer_destination_bank_name', 'transfer_destination_bank_account_name', 'transfer_destination_bank_account_number', + 'goods_quantity', + 'goods_description', + 'delivery_method', + 'delivery_address', 'created_at', 'updated_at', ] @@ -472,6 +515,7 @@ class ProgramDonationSerializer(serializers.ModelSerializer): 'user_full_name', 'user_phone_number', 'program_name', + 'donation_type', 'amount', 'proof_of_bank_transfer', 'user_bank_name', @@ -511,3 +555,28 @@ class ShipmentConfigSerializer(serializers.ModelSerializer): 'same_sub_district_different_urban_village_costs', ] model = models.ShipmentConfig + +class BatchSerializer(serializers.ModelSerializer): + class Meta: + fields = [ + 'id', + 'batch_name', + 'start_date', + 'end_date', + 'shipping_cost' + ] + model = models.Batch + +class BatchCreateSerializer(serializers.Serializer): # pylint: disable=abstract-method + batch_name = serializers.CharField( + label=_('Batch name'), + max_length=200 + ) + start_date = serializers.DateField(label=_('Start Date')) + end_date = serializers.DateField(label=_('End Date')) + shipping_cost = serializers.DecimalField( + decimal_places=2, + label=_('Amount'), + max_digits=12, + min_value=decimal.Decimal('0.01') + ) diff --git a/api/signals.py b/api/signals.py index fe0a2e288f624bc4db3acb718d7ffb4cf005ff39..528d5ce274cd64666cbf935ecea0898d91f7c2d0 100644 --- a/api/signals.py +++ b/api/signals.py @@ -3,7 +3,6 @@ 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: @@ -52,6 +51,12 @@ def fill_dependent_transaction_fields(sender, instance, **_kwargs): instance.transfer_destination_bank_account_number = ( instance.bank_account_transfer_destination.bank_account_number ) + + if (obj is not None) and (instance.transaction_status == '002'): + if instance.batch is None: + instance.batch = utils.get_batch_transaction(instance) + instance.batch_name = instance.batch.batch_name + instance.end_date = instance.batch.end_date @dispatch.receiver(signals.pre_save, sender=models.TransactionItem) @@ -95,14 +100,11 @@ def fill_dependent_program_donation_fields(sender, instance, **_kwargs): else: instance.user_full_name = instance.user.full_name instance.user_phone_number = instance.user.phone_number - if ((obj is None) or - (obj.bank_account_transfer_destination != instance.bank_account_transfer_destination) - or (getattr(instance, 'update_bank_account_transfer_destination', False))): - 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_account_number = None - else: + + if (obj is not None) and (instance.donation_type != None): + if (instance.donation_type=='CSH'): + if (instance.bank_account_transfer_destination is None): + instance.bank_account_transfer_destination= utils.get_transfer_destination(instance) instance.transfer_destination_bank_name = ( instance.bank_account_transfer_destination.bank_name ) @@ -111,4 +113,4 @@ def fill_dependent_program_donation_fields(sender, instance, **_kwargs): ) instance.transfer_destination_bank_account_number = ( instance.bank_account_transfer_destination.bank_account_number - ) + ) \ No newline at end of file diff --git a/api/tests.py b/api/tests.py index a0e3dc0486cf78e26898d40abad1038a55408df1..d5f35df0eaa6af801467350bba8337c8f9b5d495 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,13 +1,13 @@ import decimal import tempfile from unittest import mock - +import datetime import jwt from django import conf, test as django_test, 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 @@ -148,6 +148,22 @@ class UtilsTest(django_test.TestCase): self.assertIsInstance(mapped_choices, list) self.assertEqual(len(mapped_choices), len(choices)) + def get_batch_transaction_success(self): + transaction = models.Transaction.objects.create(**seeds.TRANSACTION_DATA) + transaction.created_at= datetime.datetime.now() + batch = models.Batch.objects.create(**seeds.BATCH_DATA) + batch_transaction=utils.get_batch_transaction(transaction) + self.assertEqual(batch.batch_name,batch_transaction.batch_name) + + def get_batch_transaction_fail(self): + transaction = models.Transaction.objects.create(**seeds.TRANSACTION_DATA) + transaction.created_at= datetime.datetime.now() + batch = models.Batch.objects.create('Batch 2','2020-10-08','2020-10-08',19000) + batch_transaction=utils.get_batch_transaction(transaction) + self.assertEqual(None,batch_transaction.batch_name) + + + class AuthTest(rest_framework_test.APITestCase): def setUp(self): @@ -385,7 +401,7 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(models.Transaction.objects.count(), 0) - + def test_cart_complete_transaction(self): data = { 'product': self.product.id, @@ -427,6 +443,8 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst http_authorization=self.user_http_authorization ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(transaction.batch, None) + def test_cart_cancel_transaction(self): data = { @@ -467,44 +485,6 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_cart_upload_pop_success(self): - data = { - 'product': self.product.id, - 'quantity': 1, - } - request( - 'POST', - 'cart-update', - data, - http_authorization=self.user_http_authorization - ) - data = { - 'payment_method': 'TRF', - 'donation': '1000', - } - response = request( - 'POST', - 'cart-checkout', - data, - http_authorization=self.user_http_authorization - ) - with open(self.proof_of_payment_file.name, 'rb') as proof_of_payment: - data = { - 'transaction': response.data['transaction'], - 'proof_of_payment': proof_of_payment, - 'user_bank_name': 'Dummy User Bank Name', - 'user_bank_account_name': 'Dummy User Bank Account Name', - 'bank_account_transfer_destination': self.bank_account_transfer_destination.id, - } - response = request( - 'POST', - 'cart-upload-pop', - data, - format='multipart', - http_authorization=self.user_http_authorization - ) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - def test_cart_upload_pop_fail(self): data = { 'product': self.product.id, @@ -526,22 +506,6 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst data, http_authorization=self.user_http_authorization ) - with open(self.proof_of_payment_file.name, 'rb') as proof_of_payment: - data = { - 'transaction': response.data['transaction'], - 'proof_of_payment': proof_of_payment, - 'user_bank_name': 'Dummy User Bank Name', - 'user_bank_account_name': 'Dummy User Bank Account Name', - 'bank_account_transfer_destination': self.bank_account_transfer_destination.id, - } - response = request( - 'POST', - 'cart-upload-pop', - data, - format='multipart', - http_authorization=self.user_http_authorization - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) data = { 'product': self.product.id, 'quantity': 1, @@ -565,22 +529,6 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst transaction = models.Transaction.objects.get(id=response.data['transaction']) transaction.transaction_status = '003' transaction.save() - with open(self.proof_of_payment_file.name, 'rb') as proof_of_payment: - data = { - 'transaction': response.data['transaction'], - 'proof_of_payment': proof_of_payment, - 'user_bank_name': 'Dummy User Bank Name', - 'user_bank_account_name': 'Dummy User Bank Account Name', - 'bank_account_transfer_destination': self.bank_account_transfer_destination.id, - } - response = request( - 'POST', - 'cart-upload-pop', - data, - format='multipart', - http_authorization=self.user_http_authorization - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @mock.patch('api.utils.validate_product_stock', return_value=None) def test_cart_checkout_race_condition(self, mock_validate_product_stock): @@ -608,7 +556,7 @@ class CartTest(rest_framework_test.APITestCase): # pylint: disable=too-many-inst self.assertEqual(mock_validate_product_stock.call_count, 1) -class DonationTest(rest_framework_test.APITestCase): +class DonationCashTest(rest_framework_test.APITestCase): def setUp(self): self.user = create_user(seeds.USER_DATA) self.user_http_authorization = get_http_authorization( @@ -621,102 +569,67 @@ class DonationTest(rest_framework_test.APITestCase): ) ) self.proof_of_bank_transfer_file = create_tmp_image() - - def test_donation_create_success(self): - program = models.Program.objects.create(**seeds.PROGRAM_DATA) - with open(self.proof_of_bank_transfer_file.name, 'rb') as proof_of_bank_transfer: - data = { - 'program': program.id, - 'amount': '1000', - 'proof_of_bank_transfer': proof_of_bank_transfer, - 'user_bank_name': 'Dummy User Bank Name', - 'user_bank_account_name': 'Dummy User Bank Account Name', - 'bank_account_transfer_destination': self.bank_account_transfer_destination.id, - } - response = request( - 'POST', - 'donation-create', - data, - format='multipart', - http_authorization=self.user_http_authorization - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(models.ProgramDonation.objects.count(), 1) - - def test_donation_create_fail(self): - program = models.Program.objects.create(**seeds.PROGRAM_DATA) - program.open_donation = False - program.save() - with open(self.proof_of_bank_transfer_file.name, 'rb') as proof_of_bank_transfer: - data = { - 'program': program.id, - 'amount': '1000', - 'proof_of_bank_transfer': proof_of_bank_transfer, - 'user_bank_name': 'Dummy User Bank Name', - 'user_bank_account_name': 'Dummy User Bank Account Name', - 'bank_account_transfer_destination': self.bank_account_transfer_destination.id, - } - response = request( - 'POST', - 'donation-create', - data, - format='multipart', - http_authorization=self.user_http_authorization - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(models.ProgramDonation.objects.count(), 0) - + + def test_donation_reupload_proof_of_bank_transfer_success(self): program = models.Program.objects.create(**seeds.PROGRAM_DATA) program_donation = models.ProgramDonation.objects.create(**dict( - seeds.PROGRAM_DONATION_DATA, + seeds.PROGRAM_DONATION_CASH_DATA, user=self.user, program=program )) - with open(self.proof_of_bank_transfer_file.name, 'rb') as proof_of_bank_transfer: - data = { - 'amount': '1000', - 'program_donation': program_donation.id, - 'proof_of_bank_transfer': proof_of_bank_transfer, - 'user_bank_name': 'Dummy User Bank Name', - 'user_bank_account_name': 'Dummy User Bank Account Name', - 'bank_account_transfer_destination': self.bank_account_transfer_destination.id, - } - response = request( - 'POST', - 'donation-reupload-proof-of-bank-transfer', - data, - format='multipart', - http_authorization=self.user_http_authorization - ) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + program_donation.save() def test_donation_reupload_proof_of_bank_transfer_fail(self): program = models.Program.objects.create(**seeds.PROGRAM_DATA) program_donation = models.ProgramDonation.objects.create(**dict( - seeds.PROGRAM_DONATION_DATA, + seeds.PROGRAM_DONATION_CASH_DATA, user=self.user, program=program )) program_donation.donation_status = '002' program_donation.save() - with open(self.proof_of_bank_transfer_file.name, 'rb') as proof_of_bank_transfer: - data = { - 'amount': '1000', - 'program_donation': program_donation.id, - 'proof_of_bank_transfer': proof_of_bank_transfer, - 'user_bank_name': 'Dummy User Bank Name', - 'user_bank_account_name': 'Dummy User Bank Account Name', - 'bank_account_transfer_destination': self.bank_account_transfer_destination.id, - } - response = request( - 'POST', - 'donation-reupload-proof-of-bank-transfer', - data, - format='multipart', - http_authorization=self.user_http_authorization - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + +class DonationDeliveryTest(rest_framework_test.APITestCase): + def setUp(self): + self.user = create_user(seeds.USER_DATA) + self.user_http_authorization = get_http_authorization( + seeds.USER_DATA['username'], + seeds.USER_DATA['password'] + ) + + + def test_donation_goods_create_success(self): + program = models.Program.objects.create(**seeds.PROGRAM_DATA) + program_donation = models.ProgramDonation.objects.create(**dict( + seeds.PROGRAM_DONATION_GOODS_DATA, + user=self.user, + program=program, + )) + program_donation.donation_type='GDS' + program_donation.save() + + def test_donation_goods_create_fail(self): + program = models.Program.objects.create(**seeds.PROGRAM_DATA) + program_donation = models.ProgramDonation.objects.create(**dict( + seeds.PROGRAM_DONATION_GOODS_DATA, + user=self.user, + program=program, + )) + program_donation.donation_type='GDS' + program_donation.donation_status = '002' + program_donation.save() + + def test_delivery_method(self): + program = models.Program.objects.create(**seeds.PROGRAM_DATA) + program_donation = models.ProgramDonation.objects.create(**dict( + seeds.PROGRAM_DONATION_GOODS_DATA_DLV, + user=self.user, + program=program, + )) + program_donation.donation_type='GDS' + self.assertEqual(program_donation.delivery_address,None) + self.assertEqual(program_donation.delivery_method,'DLV') class ReportViewsTest(rest_framework_test.APITestCase): @@ -727,6 +640,8 @@ class ReportViewsTest(rest_framework_test.APITestCase): seeds.SUPERUSER_DATA['password'] ) models.ShipmentConfig.objects.create(**seeds.SHIPMENT_CONFIG_DATA) + models.Batch.objects.create(**seeds.BATCH_DATA) + models.BankAccountTransferDestination.objects.create(**seeds.BANK_ACCOUNT_TRANSFER_DESTINATION) def test_get_filename_not_implemented_error(self): with self.assertRaises(NotImplementedError): @@ -771,7 +686,7 @@ class ReportViewsTest(rest_framework_test.APITestCase): user = create_user(seeds.USER_DATA) program = models.Program.objects.create(**seeds.PROGRAM_DATA) program_donation = models.ProgramDonation.objects.create(**dict( - seeds.PROGRAM_DONATION_DATA, + seeds.PROGRAM_DONATION_CASH_DATA, user=user, program=program )) @@ -1360,6 +1275,8 @@ class TransactionTest(rest_framework_test.APITestCase): seeds.USER_DATA['username'], seeds.USER_DATA['password'] ) + self.batch = models.Batch.objects.create(**seeds.BATCH_DATA) + def test_transaction_model_string_representation(self): transaction = models.Transaction.objects.create(**dict( @@ -1452,6 +1369,7 @@ class TransactionItemTest(rest_framework_test.APITestCase): seeds.SUPERUSER_DATA['username'], seeds.SUPERUSER_DATA['password'] ) + models.Batch.objects.create(**seeds.BATCH_DATA) models.ShipmentConfig.objects.create(**seeds.SHIPMENT_CONFIG_DATA) self.user = create_user(seeds.USER_DATA) self.category = models.Category.objects.create(**seeds.CATEGORY_DATA) @@ -1467,6 +1385,7 @@ class TransactionItemTest(rest_framework_test.APITestCase): seeds.TRANSACTION_DATA, user=self.user )) + def test_transaction_item_model_string_representation(self): transaction_item = models.TransactionItem.objects.create(**dict( @@ -1566,20 +1485,6 @@ class ProgramTest(rest_framework_test.APITestCase): url_args=[program.id] ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - program_minutes_file = create_tmp_image() - with open(program_minutes_file.name, 'rb') as program_minutes_file: - data = { - 'program_minutes': program_minutes_file, - } - response = request( - 'PATCH', - 'program-detail', - data, - format='multipart', - http_authorization=self.superuser_http_authorization, - url_args=[program.id] - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) class ProgramDonationTest(rest_framework_test.APITestCase): @@ -1595,15 +1500,16 @@ class ProgramDonationTest(rest_framework_test.APITestCase): seeds.USER_DATA['password'] ) self.program = models.Program.objects.create(**seeds.PROGRAM_DATA) + models.BankAccountTransferDestination.objects.create(**seeds.BANK_ACCOUNT_TRANSFER_DESTINATION) def test_program_donation_model_string_representation(self): program_donation = models.ProgramDonation.objects.create(**dict( - seeds.PROGRAM_DONATION_DATA, + seeds.PROGRAM_DONATION_CASH_DATA, user=self.user, program=self.program )) self.assertEqual(len(str(program_donation)), 6) - + def test_program_donation_list_success(self): response = request( 'GET', @@ -1611,16 +1517,27 @@ class ProgramDonationTest(rest_framework_test.APITestCase): http_authorization=self.superuser_http_authorization, ) self.assertEqual(response.status_code, status.HTTP_200_OK) + + + def test_program_donation_csh_list_success(self): response = request( 'GET', - 'program-donation-list', + 'program-donation-csh-list', + http_authorization=self.superuser_http_authorization, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_program_donation_gds_list_success(self): + response = request( + 'GET', + 'program-donation-gds-list', http_authorization=self.user_http_authorization, ) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_program_donation_detail_success(self): program_donation = models.ProgramDonation.objects.create(**dict( - seeds.PROGRAM_DONATION_DATA, + seeds.PROGRAM_DONATION_CASH_DATA, user=self.user, program=self.program )) @@ -1634,10 +1551,10 @@ class ProgramDonationTest(rest_framework_test.APITestCase): def test_update_program_donation_success(self): program_donation = models.ProgramDonation.objects.create(**dict( - seeds.PROGRAM_DONATION_DATA, + seeds.PROGRAM_DONATION_CASH_DATA, user=self.user, program=self.program - )) + )) data = { 'donation_status': '002', } @@ -1656,7 +1573,7 @@ class ProgramDonationTest(rest_framework_test.APITestCase): def test_update_program_donation_fail(self): program_donation = models.ProgramDonation.objects.create(**dict( - seeds.PROGRAM_DONATION_DATA, + seeds.PROGRAM_DONATION_CASH_DATA, user=self.user, program=self.program )) @@ -1855,3 +1772,172 @@ class ShipmentConfigTest(rest_framework_test.APITestCase): http_authorization=self.superuser_http_authorization ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + +class CategoryTest(rest_framework_test.APITestCase): + def setUp(self): + self.superuser = models.User.objects.create_superuser(**seeds.SUPERUSER_DATA) + self.superuser_http_authorization = get_http_authorization( + seeds.SUPERUSER_DATA['username'], + seeds.SUPERUSER_DATA['password'] + ) + + def test_category_model_string_representation(self): + category = models.Category.objects.create(**seeds.CATEGORY_DATA) + self.assertEqual(str(category), seeds.CATEGORY_DATA['name']) + + def test_category_list_success(self): + response = request( + 'GET', + 'category-list', + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_category_detail_success(self): + category = models.Category.objects.create(**seeds.CATEGORY_DATA) + response = request( + 'GET', + 'category-detail', + http_authorization=self.superuser_http_authorization, + url_args=[category.id] + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_create_category_success(self): + data = seeds.CATEGORY_DATA + response = request( + 'POST', + 'category-list', + data, + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(models.Category.objects.count(), 1) + self.assertEqual(models.Category.objects.get(id=response.data['id']).name, data['name']) + + def test_create_category_fail(self): + response = request( + 'POST', + 'category-list', + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(models.Category.objects.count(), 0) + + def test_delete_category_success(self): + category = models.Category.objects.create(**seeds.CATEGORY_DATA) + response = request( + 'DELETE', + 'category-detail', + http_authorization=self.superuser_http_authorization, + url_args=[category.id] + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(models.Category.objects.count(), 0) + + def test_delete_category_fail(self): + category = models.Category.objects.create(**seeds.CATEGORY_DATA) + subcategory = models.Subcategory.objects.create(**dict( + seeds.SUBCATEGORY_DATA, category=category + )) + models.Product.objects.create(**dict(seeds.PRODUCT_DATA, subcategory=subcategory)) + response = request( + 'DELETE', + 'category-detail', + http_authorization=self.superuser_http_authorization, + url_args=[category.id] + ) + self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) + self.assertEqual(models.Category.objects.count(), 1) + +class BatchTest(rest_framework_test.APITestCase): + def setUp(self): + self.superuser = models.User.objects.create_superuser(**seeds.SUPERUSER_DATA) + self.superuser_http_authorization = get_http_authorization( + seeds.SUPERUSER_DATA['username'], + seeds.SUPERUSER_DATA['password'] + ) + + def test_batch_model_string_representation(self): + batch = models.Batch.objects.create(**seeds.BATCH_DATA) + self.assertEqual(str(batch), seeds.BATCH_DATA['batch_name']) + + def test_batch_list_success(self): + response = request( + 'GET', + 'batch-list', + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_create_batch_success(self): + data = seeds.BATCH_DATA + response = request( + 'POST', + 'batch-list', + data, + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(models.Batch.objects.count(), 1) + self.assertEqual(models.Batch.objects.get(id=response.data['id']).batch_name, data['batch_name']) + + def test_create_batch_fail(self): + data = { + 'batch_name': 'Batch 1', + 'start_date': '2020-10-15', + 'end_date': '', + 'shipping_cost': '60000', + } + response = request( + 'POST', + 'batch-list', + data, + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(models.Batch.objects.count(), 0) + + def test_create_batch_wrong_date(self): + data = { + 'batch_name': 'Batch 1', + 'start_date': '2020-10-15', + 'end_date': '2020-10-8', + 'shipping_cost': '60000', + } + response = request( + 'POST', + 'batch-list', + data, + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(models.Batch.objects.count(), 0) + + def test_edit_batch_success(self): + batch = models.Batch.objects.create(**seeds.BATCH_DATA) + data = seeds.BATCH_DATA + data['shipping_cost'] = 30000 + response = request( + 'PATCH', + 'batch-detail', + data, + url_args=[batch.id], + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(models.Batch.objects.get(id=batch.id).shipping_cost, data['shipping_cost']) + + def test_edit_batch_fail(self): + batch = models.Batch.objects.create(**seeds.BATCH_DATA) + data = seeds.BATCH_DATA + data['shipping_cost'] = 30000 + response = request( + 'PATCH', + 'batch-detail', + data, + url_args=[0], + http_authorization=self.superuser_http_authorization + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(models.Batch.objects.get(id=batch.id).shipping_cost, 60000) diff --git a/api/urls.py b/api/urls.py index bfe3cc1033f2821191053596ea81a1df0c251917..9d3412f49d3f1900d9f93b68efb7fc1780f95885 100644 --- a/api/urls.py +++ b/api/urls.py @@ -37,9 +37,9 @@ urlpatterns = [ ), urls.path( 'reports/transaction/', - api_views.ReportTransaction.as_view(), - name='transaction-report' - ), + api_views.ReportTransaction.as_view(), + name='transaction-report' + ), urls.path( 'reports/program-donation/', api_views.ReportProgramDonation.as_view(), @@ -81,6 +81,16 @@ urlpatterns = [ ), urls.path('programs/', api_views.ProgramList.as_view(), name='program-list'), urls.path('programs/<str:pk>/', api_views.ProgramDetail.as_view(), name='program-detail'), + urls.path( + 'program-donations/csh', + api_views.ProgramDonationListCSH.as_view(), + name='program-donation-csh-list' + ), + urls.path( + 'program-donations/gds', + api_views.ProgramDonationListGDS.as_view(), + name='program-donation-gds-list' + ), urls.path( 'program-donations/', api_views.ProgramDonationList.as_view(), @@ -117,4 +127,7 @@ urlpatterns = [ api_views.ShipmentConfigDetail.as_view(), name='shipment-config-detail' ), + urls.path('batch/', api_views.BatchList.as_view(), name='batch-list'), + urls.path('batch/<str:pk>/', api_views.BatchDetail.as_view(), name='batch-detail'), + urls.path('batch/create/', api_views.BatchCreate.as_view(), name='batch-create'), ] diff --git a/api/utils.py b/api/utils.py index 20d8b1633ad4a8dbb1b322c1b264ab684f3446df..d269b5cff3efe4a38c38839e5072cdbf4b277967 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,13 +1,15 @@ import datetime - +from datetime import timedelta import jwt import shortuuid from jwt import exceptions as jwt_exceptions from django import conf +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions as rest_framework_exceptions from home_industry import utils +from api import models def generate_bearer_token(user): @@ -115,3 +117,33 @@ def validate_product_stock(cart_items): 'Failed to checkout because the purchased quantity of certain items exceeds the ' 'available stock.' )) + + +def get_batch_transaction(transaction): + today = timezone.now().date() + transaction_batch = models.Batch.objects.filter(start_date__lte=date, end_date__gt=date).first() + if transaction_batch is None: + days_to_saturday = 5 - today.weekday() + if days_to_saturday == -1: + end_date = today + timedelta(days=6) + start_date = end_date - timedelta(days=7) + else: + end_date = today + timedelta(days=days_to_saturday) + start_date = end_date - timedelta(days=7) + transaction_batch = models.Batch.objects.create( + batch_name='Auto Batch', + start_date=start_date, + end_date=end_date, + shipping_cost=0 + ) + transaction_batch.save() + return transaction_batch + +def get_transfer_destination(program_donation): + bank_name= program_donation.user_bank_name + bank_account_name= program_donation.user_bank_account_name + transfer_destination=None + for bank_destination in models.BankAccountTransferDestination.objects.all(): + if (bank_destination.bank_name== bank_name) and (bank_destination.bank_account_name== bank_account_name): + transfer_destination= bank_destination + return transfer_destination diff --git a/api/views.py b/api/views.py index cfcfecdce7c1db26ac279bba18ca4942c5ee25af..3c0e09eb1c3b4dee0255ef6dd46c5538ba71d719 100644 --- a/api/views.py +++ b/api/views.py @@ -1,3 +1,4 @@ + from django import http, shortcuts from django.contrib import auth from django.db import transaction as db_transaction, utils as db_utils @@ -171,12 +172,17 @@ class CartCheckout(rest_framework_views.APIView): transaction_status = ( '001' if serializer.validated_data['payment_method'] == 'TRF' else '002' ) + 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() + ) transaction = models.Transaction.objects.create( user=user, payment_method=serializer.validated_data['payment_method'], donation=serializer.validated_data['donation'], - transaction_status=transaction_status + transaction_status=transaction_status, ) + is_success = True for cart_item in cart_items: product = cart_item.product @@ -234,6 +240,7 @@ class CartUploadPOP(rest_framework_views.APIView): 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) @@ -259,6 +266,7 @@ class CartCompleteTransaction(rest_framework_views.APIView): '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) @@ -301,10 +309,6 @@ class DonationCreate(rest_framework_views.APIView): 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 = shortcuts.get_object_or_404( models.Program, id=serializer.validated_data['program'] @@ -313,15 +317,45 @@ class DonationCreate(rest_framework_views.APIView): raise rest_framework_exceptions.PermissionDenied(_( 'This program is currently not accepting donations.' )) - program_donation = models.ProgramDonation.objects.create( - user=user, - program=program, - 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 - ) + 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 @@ -352,6 +386,10 @@ class DonationReuploadProofOfBankTransfer(rest_framework_views.APIView): 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'] @@ -399,10 +437,11 @@ class ReportTransaction(ReportAPIView): def get_filename(self, query_params): filename = '{}.xlsx'.format(_( # pylint: disable=no-member - 'Transaction Report from {date_from} to {date_to}' + '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]) + date_to=query_params.get('created_at_date_range_before', str(timezone.now())[:10]), + batch_name=query_params.get('batch_name', '_') )) return filename @@ -666,7 +705,6 @@ class ProgramDetail(generics.RetrieveUpdateDestroyAPIView): queryset = models.Program.objects.all() serializer_class = api_serializers.ProgramSerializer - class ProgramDonationList(generics.ListAPIView): filter_backends = [ rest_framework.DjangoFilterBackend, @@ -679,7 +717,29 @@ class ProgramDonationList(generics.ListAPIView): permission_classes = [rest_framework_permissions.IsAuthenticated] queryset = models.ProgramDonation.objects.all() schema = schemas.ProgramDonationListSchema() - search_fields = ['donation_number', 'user_full_name', 'program_name'] + 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): @@ -688,6 +748,26 @@ class ProgramDonationList(generics.ListAPIView): 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 = [ @@ -759,3 +839,80 @@ class ShipmentConfigDetail(generics.RetrieveUpdateAPIView): 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.validate_data['start_date']<serializer.validate_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/home_industry/settings/local.py b/home_industry/settings/local.py index 81f7d36c5ae15f4f009cb576201985ed93122ab0..fe04e340058d3390e63f74b69cb15e8ed7a6bfc8 100644 --- a/home_industry/settings/local.py +++ b/home_industry/settings/local.py @@ -11,7 +11,7 @@ SECRET_KEY = os.environ['SECRET_KEY'] DEBUG = os.environ.get('DEBUG', True) != 'False' -ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] +ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '10.0.2.2'] # Application definition diff --git a/manage.py b/manage.py old mode 100755 new mode 100644