├── cart ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_remove_cart_invoice.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── cart_tags.py ├── tests.py ├── apps.py ├── admin.py ├── urls.py ├── models.py └── views.py ├── item ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_alter_item_upc.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── category_tags.py ├── tests.py ├── apps.py ├── admin.py ├── forms.py ├── templates │ └── item │ │ ├── delete.html │ │ ├── detail.html │ │ ├── service_provider_detail.html │ │ ├── create_form.html │ │ ├── update_form.html │ │ └── service_provider_item_list.html ├── urls.py ├── models.py └── views.py ├── order ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0006_merge_0003_alter_order_status_0005_alter_order_status.py │ ├── 0003_alter_order_status.py │ ├── 0004_alter_order_status.py │ ├── 0005_alter_order_status.py │ ├── 0003_auto_20210913_1956.py │ ├── 0001_initial.py │ └── 0002_auto_20210913_1544.py ├── tests.py ├── apps.py ├── admin.py ├── templates │ └── order │ │ ├── service │ │ ├── order_update.html │ │ ├── order_list.html │ │ ├── order_filter_list.html │ │ └── order_detail.html │ │ └── customer │ │ ├── order_detail.html │ │ └── order_list.html ├── filters.py ├── urls.py ├── models.py └── views.py ├── yummy ├── __init__.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── accounts ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20210831_0046.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── accounts_tags.py ├── tests.py ├── apps.py ├── templates │ └── accounts │ │ ├── service_provider │ │ ├── login.html │ │ ├── registration.html │ │ ├── change_password.html │ │ └── profile.html │ │ ├── customer │ │ ├── password_set.html │ │ ├── change_password.html │ │ ├── login_register.html │ │ ├── profile_update.html │ │ ├── password_confirm.html │ │ ├── phone_number_confirm.html │ │ └── profile.html │ │ └── base.html ├── admin.py ├── authenticate.py ├── utils.py ├── urls.py ├── models.py └── forms.py ├── address ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0006_rename_user_customeraddress_customer_user.py │ ├── 0004_auto_20210830_1327.py │ ├── 0005_auto_20210830_1329.py │ ├── 0002_auto_20210830_1305.py │ ├── 0003_auto_20210830_1305.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── templates │ └── address │ │ ├── delete_form.html │ │ ├── customer_address_list.html │ │ └── create_update_form.html ├── urls.py ├── forms.py ├── admin.py ├── models.py └── views.py ├── gateway ├── __init__.py ├── utils │ ├── __init__.py │ └── zarrinpal.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20210912_2211.py │ └── 0001_initial.py ├── tests.py ├── views.py ├── apps.py ├── admin.py └── models.py ├── library ├── __init__.py ├── models.py └── utils.py ├── payment ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_alter_invoice_address.py │ ├── 0003_auto_20210912_1657.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── urls.py ├── templates │ └── payment │ │ ├── checkout.html │ │ └── verify.html ├── forms.py ├── admin.py ├── views.py └── models.py ├── service ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0008_service_available.py │ ├── 0002_alter_service_address.py │ ├── 0003_alter_service_address.py │ ├── 0004_alter_service_address.py │ ├── 0007_auto_20210902_2056.py │ ├── 0006_auto_20210902_1943.py │ ├── 0005_auto_20210902_1522.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── service_delivary_area.py ├── tests.py ├── apps.py ├── utils.py ├── templates │ ├── service │ │ ├── service_provider │ │ │ ├── create_update_form.html │ │ │ ├── delete_form.html │ │ │ ├── list.html │ │ │ └── detail.html │ │ └── list.html │ ├── service_category │ │ ├── delete_form.html │ │ ├── create_update_form.html │ │ └── detail.html │ ├── delivery_area │ │ ├── delete_form.html │ │ └── create_update_form.html │ └── service_available_time │ │ ├── delete_form.html │ │ └── create_update_form.html ├── filters.py ├── admin.py ├── forms.py ├── urls.py └── models.py ├── requirements.txt ├── manage.py ├── LICENSE ├── templates ├── base.html └── inc │ ├── customer_navbar.html │ ├── admin_navbar.html │ └── service_provider_navbar.html ├── .gitignore └── README.md /cart/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /item/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /order/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yummy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /address/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gateway/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payment/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cart/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gateway/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /item/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /order/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /address/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cart/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gateway/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /item/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payment/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /address/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /cart/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /gateway/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /item/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /order/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /payment/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /service/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /gateway/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /cart/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CartConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'cart' 7 | -------------------------------------------------------------------------------- /item/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ItemConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'item' 7 | -------------------------------------------------------------------------------- /order/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OrderConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'order' 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.4.1 2 | Django==3.2.5 3 | django-filter==2.4.0 4 | Pillow==8.3.2 5 | psycopg2-binary==2.9.1 6 | pytz==2021.1 7 | sqlparse==0.4.2 8 | suds-jurko==0.6 9 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'accounts' 7 | -------------------------------------------------------------------------------- /address/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AddressConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'address' 7 | -------------------------------------------------------------------------------- /gateway/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GatewayConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'gateway' 7 | -------------------------------------------------------------------------------- /payment/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaymentConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'payment' 7 | -------------------------------------------------------------------------------- /service/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ServiceConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'service' 7 | -------------------------------------------------------------------------------- /item/templatetags/category_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.simple_tag 7 | def filter_category(queryset, category): 8 | return queryset.filter(category=category) 9 | -------------------------------------------------------------------------------- /accounts/templates/accounts/service_provider/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /payment/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from payment.views import CheckoutView, PaymentVerify 4 | 5 | app_name = 'payment' 6 | 7 | urlpatterns = [ 8 | path('checkout/', CheckoutView.as_view(), name='checkout'), 9 | path('verify/', PaymentVerify.as_view(), name='verify'), 10 | ] 11 | -------------------------------------------------------------------------------- /accounts/templates/accounts/service_provider/registration.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /gateway/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from gateway.models import Gateway 4 | 5 | 6 | @admin.register(Gateway) 7 | class GatewayAdmin(admin.ModelAdmin): 8 | list_display = ('title', 'gateway_code', 'is_enable') 9 | list_filter = ('is_enable',) 10 | search_fields = ('title',) 11 | 12 | -------------------------------------------------------------------------------- /library/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class BaseModel(models.Model): 5 | created_time = models.DateTimeField(verbose_name='created time', auto_now_add=True) 6 | modified_time = models.DateTimeField(verbose_name='modified time', auto_now=True) 7 | 8 | class Meta: 9 | abstract = True 10 | -------------------------------------------------------------------------------- /order/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Order 4 | 5 | 6 | @admin.register(Order) 7 | class OrderAdmin(admin.ModelAdmin): 8 | list_display = ('customer', 'status') 9 | list_filter = ('status',) 10 | list_editable = ('status',) 11 | search_fields = ('customer__phone_number',) 12 | -------------------------------------------------------------------------------- /payment/templates/payment/checkout.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ address_form.as_p }} 6 | {{ gateway_form.as_p }} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /accounts/templates/accounts/service_provider/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block content %} 3 |

Change your password

4 |
5 | {% csrf_token %} 6 | {{ form.as_p }} 7 | 8 |
9 | {% endblock %} -------------------------------------------------------------------------------- /service/utils.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from accounts.utils import IsServiceProvider 4 | 5 | 6 | class CustomServiceIsServiceProvider(ABC, IsServiceProvider): 7 | def test_func(self): 8 | result = super().test_func() 9 | obj = self.get_object() 10 | return obj.service_provider == self.request.user and result 11 | -------------------------------------------------------------------------------- /service/templatetags/service_delivary_area.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.simple_tag() 7 | def delivery_area_string(service): 8 | area_list = list() 9 | for delivery in service.delivery_areas.all(): 10 | area_list.append(delivery.area.name) 11 | return ', '.join(area_list) 12 | -------------------------------------------------------------------------------- /order/migrations/0006_merge_0003_alter_order_status_0005_alter_order_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-16 08:59 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('order', '0003_alter_order_status'), 10 | ('order', '0005_alter_order_status'), 11 | ] 12 | 13 | operations = [ 14 | ] 15 | -------------------------------------------------------------------------------- /cart/migrations/0002_remove_cart_invoice.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-12 12:27 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('cart', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='cart', 15 | name='invoice', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /order/templates/order/service/order_update.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | cancel 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /cart/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from cart.models import Cart, CartLine 4 | 5 | 6 | class CartLineInline(admin.TabularInline): 7 | model = CartLine 8 | extra = 0 9 | 10 | 11 | @admin.register(Cart) 12 | class CartAdmin(admin.ModelAdmin): 13 | list_display = ('customer', 'is_paid') 14 | list_filter = ('is_paid',) 15 | search_fields = ('customer__phone_number',) 16 | inlines = [CartLineInline] 17 | -------------------------------------------------------------------------------- /payment/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from address.models import CustomerAddress 4 | from gateway.models import Gateway 5 | 6 | 7 | class AddressSelectForm(forms.Form): 8 | address = forms.ModelChoiceField(queryset=CustomerAddress.objects.all(), widget=forms.RadioSelect) 9 | 10 | 11 | class GatewaySelectForm(forms.Form): 12 | gateway = forms.ModelChoiceField(queryset=Gateway.objects.filter(is_enable=True), widget=forms.RadioSelect) 13 | -------------------------------------------------------------------------------- /service/templates/service/service_provider/create_update_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | cancel 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /yummy/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for yummy project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /yummy/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for yummy project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /address/templates/address/delete_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 | {% csrf_token %} 6 |

Are you sure to delete address ({{ object.state }} | {{ object.area }})?

7 | 8 |
9 | cancel 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /service/filters.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | import django_filters 4 | 5 | from service.models import Service 6 | 7 | 8 | class ServiceFilter(django_filters.FilterSet): 9 | name = django_filters.CharFilter( 10 | widget=forms.TextInput(attrs={'class': 'form-control'}), lookup_expr='icontains' 11 | ) 12 | 13 | service_type = django_filters.ChoiceFilter( 14 | choices=Service.SERVICE_TYPES, 15 | widget=forms.Select(attrs={'class': 'form-control'}) 16 | ) 17 | -------------------------------------------------------------------------------- /address/migrations/0006_rename_user_customeraddress_customer_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-31 10:06 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('address', '0005_auto_20210830_1329'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='customeraddress', 15 | old_name='user', 16 | new_name='customer_user', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /service/templates/service/service_provider/delete_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 | {% csrf_token %} 6 |

Are you sure to delete service ({{ service.name }})?

7 | 8 |
9 | cancel 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /item/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from item.models import Item, ItemLine 4 | 5 | 6 | class ItemLineInline(admin.TabularInline): 7 | model = ItemLine 8 | max_num = 1 9 | 10 | 11 | @admin.register(Item) 12 | class ItemAdmin(admin.ModelAdmin): 13 | list_display = ('name', 'upc', 'service', 'category', 'price', 'stock', 'available') 14 | list_editable = ('available',) 15 | list_filter = ('available', 'created_time') 16 | search_fields = ('upc', 'name') 17 | inlines = [ItemLineInline] -------------------------------------------------------------------------------- /item/migrations/0002_alter_item_upc.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-01 08:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('item', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='item', 15 | name='upc', 16 | field=models.BigIntegerField(db_index=True, editable=False, unique=True, verbose_name='upc'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /service/migrations/0008_service_available.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-02 16:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('service', '0007_auto_20210902_2056'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='service', 15 | name='available', 16 | field=models.BooleanField(default=False, verbose_name='available'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cart/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from cart.views import AddToCartView, CartLineDeleteView, CartLineDecreaseView, EmptyCartView 4 | 5 | app_name = 'cart' 6 | 7 | urlpatterns = [ 8 | path('add/', AddToCartView.as_view(), name='add-to-cart'), 9 | path('cartline/delete//', CartLineDeleteView.as_view(), name='cart-line-delete'), 10 | path('cartline/decrease//', CartLineDecreaseView.as_view(), name='cart-line-decrease'), 11 | path('cart-empty/', EmptyCartView.as_view(), name='empty-cart'), 12 | 13 | ] 14 | -------------------------------------------------------------------------------- /item/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from item.models import Item 4 | 5 | 6 | class ItemCreateForm(forms.ModelForm): 7 | quantity = forms.IntegerField(initial=0) 8 | 9 | class Meta: 10 | model = Item 11 | fields = ('name', 'price', 'description', 'available', 'image') 12 | 13 | 14 | class ItemUpdateForm(forms.ModelForm): 15 | quantity = forms.IntegerField(initial=0) 16 | 17 | class Meta: 18 | model = Item 19 | fields = ('category', 'name', 'price', 'description', 'available', 'image') 20 | -------------------------------------------------------------------------------- /order/filters.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | import django_filters 4 | 5 | from order.models import Order 6 | 7 | 8 | class OrderFilter(django_filters.FilterSet): 9 | created_time = django_filters.DateFilter( 10 | widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date', 'data-date-format': 'YYYY-MMMM-DD '}), 11 | lookup_expr='contains' 12 | ) 13 | 14 | status = django_filters.ChoiceFilter( 15 | choices=Order.STATUS, 16 | widget=forms.Select(attrs={'class': 'form-control'}) 17 | ) -------------------------------------------------------------------------------- /accounts/templates/accounts/service_provider/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |

Profile

4 |

Your username : {{ request.user.username }}

5 |

Your email : {{ request.user.email }}

6 |

Your phone number : {{ request.user.phone_number }}

7 |

8 | Service List 9 |

10 | Change password 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /service/templates/service_category/delete_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 | {% csrf_token %} 6 |

Are you sure to delete service category ({{ category.name }})?

7 | 8 |
9 | cancel 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /service/templates/delivery_area/delete_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 | {% csrf_token %} 6 |

Are you sure to delete service category ({{ delivery.area.name }})?

7 | 8 |
9 | cancel 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /item/templates/item/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 | {% csrf_token %} 6 |

Are you sure to delete this item from your menu?

7 |

{{ object.name }}

8 | 9 | 10 |
11 |
12 | Cancel 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /order/migrations/0003_alter_order_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-15 09:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('order', '0002_auto_20210913_1544'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='order', 15 | name='status', 16 | field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (1, 'sending')], default=0, verbose_name='status'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /order/migrations/0004_alter_order_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-13 15:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('order', '0003_auto_20210913_1956'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='order', 15 | name='status', 16 | field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (2, 'sending'), (2, 'delivered')], default=0, verbose_name='status'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /order/migrations/0005_alter_order_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-13 15:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('order', '0004_alter_order_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='order', 15 | name='status', 16 | field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (1, 'sending'), (2, 'delivered')], default=0, verbose_name='status'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /service/templates/service_available_time/delete_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 | {% csrf_token %} 6 |

Are you sure to delete service available time ({{ available_time.get_day_display }})?

7 | 8 |
9 | cancel 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /gateway/migrations/0002_auto_20210912_2211.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-12 17:41 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gateway', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='gateway', 15 | options={'verbose_name': 'Gateway', 'verbose_name_plural': 'Gateways'}, 16 | ), 17 | migrations.AlterModelTable( 18 | name='gateway', 19 | table='gateway', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /payment/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from payment.models import Invoice, Payment 4 | 5 | 6 | @admin.register(Invoice) 7 | class InvoiceAdmin(admin.ModelAdmin): 8 | list_display = ('customer', 'price', 'is_paid', 'address') 9 | list_filter = ('is_paid', 'created_time') 10 | search_fields = ('customer__phone_number',) 11 | 12 | 13 | @admin.register(Payment) 14 | class PaymentAdmin(admin.ModelAdmin): 15 | list_display = ('uuid', 'customer', 'gateway', 'price', 'is_paid') 16 | list_filter = ('is_paid', 'created_time', 'gateway') 17 | search_fields = ('uuid', 'customer__phone_number') 18 | -------------------------------------------------------------------------------- /service/templates/service_category/create_update_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | {% if service %} 9 | cancel 10 | {% else %} 11 | cancel 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /service/templates/delivery_area/create_update_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | {% if object %} 9 | cancel 11 | {% else %} 12 | cancel 14 | {% endif %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /service/templates/service_available_time/create_update_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | {% if object %} 9 | cancel 11 | {% else %} 12 | cancel 14 | {% endif %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /service/migrations/0002_alter_service_address.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-30 08:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('address', '0002_auto_20210830_1305'), 11 | ('service', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='service', 17 | name='address', 18 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /accounts/templatetags/accounts_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.auth import get_user_model 3 | 4 | from accounts.models import Customer, ServiceProvider 5 | 6 | User = get_user_model() 7 | 8 | register = template.Library() 9 | 10 | 11 | @register.simple_tag 12 | def is_customer(user): 13 | if isinstance(user, Customer): 14 | return True 15 | return False 16 | 17 | 18 | @register.simple_tag 19 | def is_service_provider(user): 20 | if isinstance(user, ServiceProvider): 21 | return True 22 | return False 23 | 24 | 25 | @register.simple_tag 26 | def is_admin_user(user): 27 | if isinstance(user, User): 28 | return True 29 | return False 30 | -------------------------------------------------------------------------------- /item/templates/item/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} {{ item.name }} {% endblock %} 3 | {% block content %} 4 |

{{ item.name }}

5 |

Category : {{ item.category.name }}

6 | {% if item.image %} 7 | {{ item.name }} image 9 | {% endif %} 10 |

Description : {{ item.description }}

11 |

Price : {{ item.price }}

12 | Add to cart

13 | Back to List 15 | {% endblock %} -------------------------------------------------------------------------------- /payment/migrations/0002_alter_invoice_address.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-30 08:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('address', '0002_auto_20210830_1305'), 11 | ('payment', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='invoice', 17 | name='address', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.customeraddress', verbose_name='address'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /order/migrations/0003_auto_20210913_1956.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-13 15:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('order', '0002_auto_20210913_1544'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='order', 15 | name='is_delivered', 16 | ), 17 | migrations.AlterField( 18 | model_name='order', 19 | name='status', 20 | field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending'), (2, 'delivered')], default=0, verbose_name='status'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /item/templates/item/service_provider_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} {{ item.name }} {% endblock %} 3 | {% block content %} 4 |

{{ item.name }}

5 | {% if item.image %} 6 | {{ item.name }} image 8 | {% endif %} 9 |

Stock : {{ item.stock }}

10 |

Category : {{ item.category.name }}

11 |

Description : {{ item.description }}

12 |

Price : {{ item.price }}


13 | Back to List 15 | {% endblock %} -------------------------------------------------------------------------------- /service/migrations/0003_alter_service_address.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-31 11:56 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('address', '0006_rename_user_customeraddress_customer_user'), 11 | ('service', '0002_alter_service_address'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='service', 17 | name='address', 18 | field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /service/migrations/0004_alter_service_address.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-31 11:58 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('address', '0006_rename_user_customeraddress_customer_user'), 11 | ('service', '0003_alter_service_address'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='service', 17 | name='address', 18 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /accounts/migrations/0002_auto_20210831_0046.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-30 20:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='customer', 15 | name='phone_number', 16 | field=models.CharField(max_length=12, unique=True, verbose_name='phone number'), 17 | ), 18 | migrations.AlterField( 19 | model_name='serviceprovider', 20 | name='phone_number', 21 | field=models.CharField(max_length=12, unique=True, verbose_name='phone number'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /cart/templatetags/cart_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.db.models import Sum, F 3 | 4 | 5 | from cart.models import Cart 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.simple_tag 11 | def get_cart(request, service): 12 | try: 13 | cart = Cart.objects.get(id=request.COOKIES.get('cart_id', None), is_paid=False) 14 | except Cart.DoesNotExist: 15 | return None 16 | else: 17 | if cart.lines.filter(item__service=service).exists(): 18 | return cart 19 | return None 20 | 21 | 22 | @register.simple_tag 23 | def calculate_total_price(cart): 24 | price = cart.lines.annotate(true_price=(F('quantity') * F('item__price'))).aggregate(Sum('true_price')) 25 | return price['true_price__sum'] 26 | -------------------------------------------------------------------------------- /payment/templates/payment/verify.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Payment Verification{% endblock %} 3 | {% block content %} 4 | {% if is_paid %} 5 | 6 |
7 |

Payment was successful

8 |
Your ref code: {{ ref_id }}
9 |
10 | {% else %} 11 |
12 |

Payment was successful

13 |
Your ref code: {{ ref_id }}
14 |
15 | {% endif %} 16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /item/templates/item/create_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} Add item {% endblock %} 3 | {% block content %} 4 |

Add new item

5 |
6 | {% csrf_token %} 7 | {% for field in form %} 8 | 9 |
10 |

{{ field }}

11 | {% for error in field.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 | {% endfor %} 15 |
16 | {% if form.errors %} 17 | {% block form_error %} {% endblock %} 18 | {% endif %} 19 | 20 | 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /accounts/templates/accounts/customer/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block title %} Password Set {% endblock %} 3 | {% block content %} 4 |

Set Password

5 |
6 | {% csrf_token %} 7 | {% for field in form %} 8 | 9 |
10 |

{{ field }}

11 | {% for error in field.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 | {% endfor %} 15 |
16 | {% if form.errors %} 17 | {% block form_error %} {% endblock %} 18 | {% endif %} 19 | 20 | 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /item/templates/item/update_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} Update Item {% endblock %} 3 | {% block content %} 4 |

Update item

5 |
6 | {% csrf_token %} 7 | {% for field in form %} 8 | 9 |
10 |

{{ field }}

11 | {% for error in field.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 | {% endfor %} 15 |
16 | {% if form.errors %} 17 | {% block form_error %} {% endblock %} 18 | {% endif %} 19 | 20 | 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /accounts/templates/accounts/customer/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block title %} Change Password {% endblock %} 3 | {% block content %} 4 |

Change your password

5 |
6 | {% csrf_token %} 7 | {% for field in form %} 8 | 9 |
10 |

{{ field }}

11 | {% for error in field.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 | {% endfor %} 15 |
16 | {% if form.errors %} 17 | {% block form_error %} {% endblock %} 18 | {% endif %} 19 | 20 | 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /accounts/templates/accounts/customer/login_register.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block title %} Login - Register {% endblock %} 3 | {% block content %} 4 |

Register - Login

5 |
6 | {% csrf_token %} 7 | {% for field in form %} 8 | 9 |
10 |

{{ field }}

11 | {% for error in field.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 | {% endfor %} 15 |
16 | {% if form.errors %} 17 | {% block form_error %} {% endblock %} 18 | {% endif %} 19 | 20 | 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /accounts/templates/accounts/customer/profile_update.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block title %} Profile Update {% endblock %} 3 | {% block content %} 4 |

Update your profile

5 |
6 | {% csrf_token %} 7 | {% for field in form %} 8 | 9 |
10 |

{{ field }}

11 | {% for error in field.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 | {% endfor %} 15 |
16 | {% if form.errors %} 17 | {% block form_error %} {% endblock %} 18 | {% endif %} 19 | 20 | 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /accounts/templates/accounts/customer/password_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block title %} Password Confirm {% endblock %} 3 | {% block content %} 4 |

Confirm your password

5 |
6 | {% csrf_token %} 7 | {% for field in form %} 8 | 9 |
10 |

{{ field }}

11 | {% for error in field.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 | {% endfor %} 15 |
16 | {% if form.errors %} 17 | {% block form_error %} {% endblock %} 18 | {% endif %} 19 | 20 | 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /accounts/templates/accounts/customer/phone_number_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'accounts/base.html' %} 2 | {% block title %} Phone number confirm {% endblock %} 3 | {% block content %} 4 |

Confirm your phone number

5 |
6 | {% csrf_token %} 7 | {% for field in form %} 8 | 9 |
10 |

{{ field }}

11 | {% for error in field.errors %} 12 |

{{ error }}

13 | {% endfor %} 14 | {% endfor %} 15 |
16 | {% if form.errors %} 17 | {% block form_error %} {% endblock %} 18 | {% endif %} 19 | 20 | 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /gateway/utils/zarrinpal.py: -------------------------------------------------------------------------------- 1 | from suds.client import Client 2 | 3 | 4 | def zarrinpal_request_handler(amount, description, user_email, user_phone_number, REQUEST_URL, MERCHANT_ID, CALL_BACK): 5 | client = Client(REQUEST_URL) 6 | result = client.service.PaymentRequest( 7 | MERCHANT_ID, amount, description, user_email, user_phone_number, CALL_BACK 8 | ) 9 | if result.Status == 100: 10 | return 'https://sandbox.zarinpal.com/pg/StartPay/' + result.Authority, result.Authority 11 | else: 12 | return None, None 13 | 14 | 15 | def zarrinpal_payment_verify(amount, authority, REQUEST_URL, MERCHANT_ID,): 16 | client = Client(REQUEST_URL) 17 | result = client.service.PaymentVerification(MERCHANT_ID, authority, amount) 18 | is_paid = True if result.Status in (100, 101) else False 19 | return is_paid, result.RefID 20 | -------------------------------------------------------------------------------- /order/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from order.views import OrderServiceListView, OrderServiceDetailView, OrderServiceUpdateView, OrderServiceListFilterView 4 | from order.views import CustomerOrdersListView, CustomerOrderDetailView 5 | 6 | app_name = 'order' 7 | 8 | urlpatterns = [ 9 | path('service/list//', OrderServiceListFilterView.as_view(), name='service-order-list'), 10 | path('service/today//', OrderServiceListView.as_view(), name='service-order-today'), 11 | path('service/detail//', OrderServiceDetailView.as_view(), name='service-order-detail'), 12 | path('service/update//', OrderServiceUpdateView.as_view(), name='service-order-update'), 13 | 14 | path('customer/list/', CustomerOrdersListView.as_view(), name='customer-list'), 15 | path('customer//detail/', CustomerOrderDetailView.as_view(), name='customer-detail'), 16 | ] -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.hashers import make_password 3 | 4 | from accounts.models import Customer, ServiceProvider 5 | 6 | 7 | @admin.register(Customer) 8 | class CustomerAdmin(admin.ModelAdmin): 9 | list_display = ('phone_number', 'first_name', 'last_name', 'date_joined', 'is_active') 10 | list_filter = ('is_active', 'date_joined') 11 | search_fields = ('phone_number',) 12 | 13 | def save_model(self, request, obj, form, change): 14 | obj.password = make_password(form.cleaned_data['password']) 15 | return super().save_model(request, obj, form, change) 16 | 17 | 18 | @admin.register(ServiceProvider) 19 | class ServiceProviderAdmin(admin.ModelAdmin): 20 | list_display = ('username', 'email', 'phone_number', 'date_joined', 'is_active') 21 | list_filter = ('is_active', 'date_joined') 22 | search_fields = ('username', 'phone_number') 23 | -------------------------------------------------------------------------------- /address/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import CustomerAddressCreateView, CustomerAddressUpdateView, CustomerAddressDeleteView, \ 4 | ServiceAddressCreateView, ServiceAddressUpdateView, CustomerAddressListView 5 | 6 | app_name = 'address' 7 | 8 | urlpatterns = [ 9 | path('customer/create/', CustomerAddressCreateView.as_view(), name='customer-address-create'), 10 | path('service/create//', ServiceAddressCreateView.as_view(), name='service-address-create'), 11 | 12 | path('customer/update//', CustomerAddressUpdateView.as_view(), name='customer-address-update'), 13 | path('service/update//', ServiceAddressUpdateView.as_view(), name='service-address-update'), 14 | 15 | path('customer/delete//', CustomerAddressDeleteView.as_view(), name='customer-address-delete'), 16 | path('customer/list/', CustomerAddressListView.as_view(), name='customer-address-list'), 17 | ] 18 | -------------------------------------------------------------------------------- /item/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from item.views import ItemUpdateView, ItemDetailView, ItemDeleteView, ServiceProviderItemDetailView, ItemCreateView, \ 3 | ItemListView, ServiceCategoryItemListView 4 | 5 | app_name = 'item' 6 | 7 | urlpatterns = [ 8 | path('create///', ItemCreateView.as_view(), name='create'), 9 | path('update//', ItemUpdateView.as_view(), name='update'), 10 | path('detail//', ItemDetailView.as_view(), name='detail'), 11 | path('detail-serviceprovider//', ServiceProviderItemDetailView.as_view(), name='detail-service-provider'), 12 | path('delete//', ItemDeleteView.as_view(), name='delete'), 13 | path('/list/', ItemListView.as_view(), name='list'), 14 | path( 15 | 'serviceprovider///list/', ServiceCategoryItemListView.as_view(), 16 | name='service-provider-list'), 17 | ] 18 | -------------------------------------------------------------------------------- /service/migrations/0007_auto_20210902_2056.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-02 16:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('service', '0006_auto_20210902_1943'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='service', 15 | options={'ordering': ('created_time',), 'verbose_name': 'Service', 'verbose_name_plural': 'Services'}, 16 | ), 17 | migrations.AlterField( 18 | model_name='service', 19 | name='banner', 20 | field=models.ImageField(blank=True, null=True, upload_to='service/banners/', verbose_name='banner'), 21 | ), 22 | migrations.AlterField( 23 | model_name='service', 24 | name='logo', 25 | field=models.ImageField(blank=True, null=True, upload_to='service/logos/', verbose_name='logo'), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /service/templates/service_category/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |

Category Detail

5 |

Category name : {{ category.name }}

6 |
7 |

Service Items

8 | {% if category.items.all %} 9 |
    10 | {% for item in category.items.all %} 11 |
  • 12 | {{ category.name }} 13 |
    14 | Update 15 | Delete 16 |
    17 |
  • 18 | {% endfor %} 19 |
20 | {% else %} 21 |

Your category yet not has item!Create Item

22 | {% endif %} 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /service/migrations/0006_auto_20210902_1943.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-02 15:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('service', '0005_auto_20210902_1522'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='serviceavailabletime', 15 | options={'ordering': ('day',), 'verbose_name': 'ServiceAvailableTime', 'verbose_name_plural': 'ServiceAvailableTimes'}, 16 | ), 17 | migrations.AddField( 18 | model_name='service', 19 | name='banner', 20 | field=models.ImageField(blank=True, null=True, upload_to='service/banner', verbose_name='banner'), 21 | ), 22 | migrations.AddField( 23 | model_name='service', 24 | name='logo', 25 | field=models.ImageField(blank=True, null=True, upload_to='service/logo', verbose_name='logo'), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /service/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Service, ServiceCategory, DeliveryArea, ServiceAvailableTime 3 | 4 | 5 | @admin.register(Service) 6 | class ServiceAdmin(admin.ModelAdmin): 7 | list_display = ('uuid', 'name', 'service_type', 'minimum_purchase', 'address') 8 | list_filter = ('service_type',) 9 | search_fields = ('name',) 10 | 11 | 12 | @admin.register(ServiceCategory) 13 | class ServiceCategoryAdmin(admin.ModelAdmin): 14 | list_display = ('name', 'slug', 'service') 15 | search_fields = ('name', 'service') 16 | 17 | 18 | @admin.register(DeliveryArea) 19 | class DeliveryAreaAdmin(admin.ModelAdmin): 20 | list_display = ('service', 'area') 21 | search_fields = ('service', 'area') 22 | 23 | 24 | @admin.register(ServiceAvailableTime) 25 | class ServiceAvailableTimeAdmin(admin.ModelAdmin): 26 | list_display = ('service', 'day', 'open_time', 'close_time', 'is_close') 27 | list_filter = ('day', 'is_close') 28 | search_fields = ('service',) 29 | 30 | 31 | -------------------------------------------------------------------------------- /address/migrations/0004_auto_20210830_1327.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-30 08:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('address', '0003_auto_20210830_1305'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='customeraddress', 15 | name='alley', 16 | field=models.TextField(blank=True), 17 | ), 18 | migrations.AddField( 19 | model_name='customeraddress', 20 | name='street', 21 | field=models.TextField(blank=True), 22 | ), 23 | migrations.AddField( 24 | model_name='serviceaddress', 25 | name='alley', 26 | field=models.TextField(blank=True), 27 | ), 28 | migrations.AddField( 29 | model_name='serviceaddress', 30 | name='street', 31 | field=models.TextField(blank=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /accounts/templates/accounts/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 12 | 13 | {% block title %} {% endblock %} 14 | 15 | 16 |
17 | {% if messages %} 18 | {% for message in messages %} 19 | 20 | {% endfor %} 21 | {% endif %} 22 | {% block content %} 23 | {% endblock %} 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /address/templates/address/customer_address_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | Create new address

4 | 5 |
    6 | {% for address in addresses %} 7 |
  • 8 | {{ address.state.name }} - {{ address.city.name }} - {{ address.area.name }} - {{ address.street }} 9 | - {{ address.alley }} - {{ address.floor }} - {{ address.plaque }} 10 |
    11 | Update 13 | Delete 15 |
    16 |
  • 17 | {% endfor %} 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /address/migrations/0005_auto_20210830_1329.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-30 08:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('address', '0004_auto_20210830_1327'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='customeraddress', 15 | name='alley', 16 | field=models.CharField(max_length=30), 17 | ), 18 | migrations.AlterField( 19 | model_name='customeraddress', 20 | name='street', 21 | field=models.CharField(max_length=50), 22 | ), 23 | migrations.AlterField( 24 | model_name='serviceaddress', 25 | name='alley', 26 | field=models.CharField(max_length=30), 27 | ), 28 | migrations.AlterField( 29 | model_name='serviceaddress', 30 | name='street', 31 | field=models.CharField(max_length=50), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /library/utils.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from django.contrib.auth.mixins import UserPassesTestMixin 4 | from django.core.exceptions import PermissionDenied 5 | from django.http import HttpResponseRedirect 6 | 7 | 8 | class CustomUserPasses(ABC, UserPassesTestMixin): 9 | raise_exception = True 10 | 11 | def handle_no_permission(self, path_url=None, redirect=False): 12 | if redirect: 13 | return HttpResponseRedirect(path_url) 14 | 15 | else: 16 | raise PermissionDenied(self.get_permission_denied_message()) 17 | 18 | def dispatch(self, request, *args, **kwargs): 19 | user_test_result = self.test_func() 20 | 21 | if isinstance(self.test_func(), tuple): 22 | user_test_bool, redirect, path_url = user_test_result 23 | if not user_test_bool: 24 | return self.handle_no_permission(path_url, redirect) 25 | 26 | if not user_test_result: 27 | return self.handle_no_permission() 28 | return super().dispatch(request, *args, **kwargs) 29 | -------------------------------------------------------------------------------- /address/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from address.models import CustomerAddress, ServiceAddress 4 | 5 | base_fields = ('state', 'city', 'area', 'street', 'alley', 'floor', 'plaque') 6 | base_widgets = { 7 | 'state': forms.Select(attrs={'class': 'form-control'}), 8 | 'city': forms.Select(attrs={'class': 'form-control'}), 9 | 'area': forms.Select(attrs={'class': 'form-control'}), 10 | 'street': forms.TextInput(attrs={'class': 'form-control'}), 11 | 'alley': forms.TextInput(attrs={'class': 'form-control'}), 12 | 'floor': forms.NumberInput(attrs={'class': 'form-control'}), 13 | 'plaque': forms.NumberInput(attrs={'class': 'form-control'}), 14 | } 15 | 16 | 17 | class CustomerAddressCreateUpdateForm(forms.ModelForm): 18 | class Meta: 19 | model = CustomerAddress 20 | fields = base_fields 21 | widgets = base_widgets 22 | 23 | 24 | class ServiceAddressCreateUpdateForm(forms.ModelForm): 25 | class Meta: 26 | model = ServiceAddress 27 | fields = base_fields 28 | widgets = base_widgets 29 | -------------------------------------------------------------------------------- /address/templates/address/create_update_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load accounts_tags %} 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | {{ form.as_p }} 7 | 8 |
9 | {% is_service_provider request.user as is_provider %} 10 | {% is_customer request.user as is_customer %} 11 | {% if is_provider %} 12 | {% if service %} 13 | cancel 15 | 16 | {% else %} 17 | cancel 19 | {% endif %} 20 | {% elif is_customer %} 21 | cancel 23 | {% endif %} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 JaMedadi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /accounts/templates/accounts/customer/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} Profile {% endblock %} 3 | {% block content %} 4 |

Profile

5 |

Phone number : {{ request.user.phone_number }}

6 |

First name : {{ request.user.first_name }}

7 |

Last name : {{ request.user.last_name }}

8 |
9 | Address list

10 | Orders

11 | Create new address

12 | 13 | Update profile

14 | 15 | {% if not request.user.password %} 16 | Set password 17 |

18 | {% else %} 19 | Change password 20 |

21 | {% endif %} 22 | 23 | {% endblock %} -------------------------------------------------------------------------------- /address/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import State, City, Area, ServiceAddress, CustomerAddress 3 | 4 | 5 | @admin.register(State) 6 | class StateAdmin(admin.ModelAdmin): 7 | list_display = ('name', 'slug') 8 | search_fields = ('name',) 9 | 10 | 11 | @admin.register(City) 12 | class CityAdmin(admin.ModelAdmin): 13 | list_display = ('name', 'slug', 'state') 14 | list_filter = ('state',) 15 | search_fields = ('name',) 16 | 17 | 18 | @admin.register(Area) 19 | class AreaAdmin(admin.ModelAdmin): 20 | list_display = ('name', 'slug', 'city') 21 | list_filter = ('city',) 22 | search_fields = ('name',) 23 | 24 | 25 | @admin.register(ServiceAddress) 26 | class ServiceAddressAdmin(admin.ModelAdmin): 27 | list_display = ('state', 'city', 'area', 'floor', 'plaque') 28 | list_filter = ('state', 'city') 29 | search_fields = ('area', 'floor', 'plaque') 30 | 31 | 32 | @admin.register(CustomerAddress) 33 | class CustomerAddressAdmin(admin.ModelAdmin): 34 | list_display = ('customer_user', 'state', 'city', 'area', 'floor', 'plaque') 35 | list_filter = ('state', 'city') 36 | search_fields = ('area', 'floor', 'plaque') 37 | -------------------------------------------------------------------------------- /order/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from accounts.models import Customer 4 | from library.models import BaseModel 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from payment.models import Invoice 8 | 9 | 10 | class Order(BaseModel): 11 | PREPARING_FOOD = 0 12 | SENDING = 1 13 | DELIVERED = 2 14 | STATUS = ( 15 | (PREPARING_FOOD, _('preparing food')), 16 | (SENDING, _('sending')), 17 | (DELIVERED, _('delivered')) 18 | ) 19 | invoice = models.OneToOneField(Invoice, verbose_name=_('invoice'), related_name='order', on_delete=models.PROTECT) 20 | customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='orders', on_delete=models.PROTECT) 21 | status = models.PositiveSmallIntegerField(verbose_name=_('status'), choices=STATUS, default=PREPARING_FOOD) 22 | 23 | class Meta: 24 | verbose_name = _('Order') 25 | verbose_name_plural = _('Orders') 26 | db_table = 'order' 27 | 28 | def __str__(self): 29 | return f"{self.customer} - {self.status}" 30 | 31 | @classmethod 32 | def create(cls, invoice): 33 | cls.objects.create(invoice=invoice, customer=invoice.customer) 34 | -------------------------------------------------------------------------------- /order/templates/order/service/order_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Payment Verification{% endblock %} 3 | {% block content %} 4 |
Today: {% now "Y-m-d" %}
5 |
6 | {% for order in orders %} 7 |
8 |
9 |
10 |
11 |

id : {{ order.id }}

12 |

status : {{ order.get_status_display }}

13 |

date time : {{ order.created_time }}

14 |

total price : {{ order.invoice.price }}$

15 |
16 |
17 |
18 | Show Factor 20 |
21 |
22 |
23 | {% endfor %} 24 |
25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /order/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-28 20:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('payment', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Order', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 21 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 22 | ('status', models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending')], verbose_name='status')), 23 | ('is_delivered', models.BooleanField(default=False, verbose_name='is delivered')), 24 | ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='payment.invoice', verbose_name='invoice')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /order/templates/order/customer/order_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} Order Detail {% endblock %} 3 | 4 | {% block content %} 5 |

{{ order.invoice.cart.service }}

6 |

7 |

status : {{ order.get_status_display }}

8 |

order date time : {{ order.created_time }}

9 |

address : {{ order.invoice.address.state.name }} - {{ order.invoice.address.city.name }} 10 | - {{ order.invoice.address.area.name }} - {{ order.invoice.address.street }} 11 | - {{ order.invoice.address.alley }} - {{ order.invoice.address.floor }} 12 | - {{ order.invoice.address.plaque }} 13 |

14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for line in order.invoice.cart.lines.all %} 26 | 27 | 28 | 29 | 30 | 31 | {% endfor %} 32 | 33 | 34 |
ItemQuantityPrice
{{ line.item }}{{ line.quantity }}$ {{ line.price }}
35 | 36 |
Total price : {{ order.invoice.price }}
37 | 38 | {% endblock %} -------------------------------------------------------------------------------- /gateway/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-12 12:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Gateway', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 19 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 20 | ('title', models.CharField(max_length=100, verbose_name='title')), 21 | ('gateway_request_url', models.CharField(blank=True, max_length=150, verbose_name='request url')), 22 | ('gateway_verify_url', models.CharField(blank=True, max_length=150, verbose_name='verify url')), 23 | ('gateway_code', models.CharField(choices=[('zarrinpal', 'Zarrinpal'), ('saman', 'Saman')], max_length=20, verbose_name='gateway code')), 24 | ('is_enable', models.BooleanField(default=True, verbose_name='is enable')), 25 | ('auth_data', models.TextField(blank=True, verbose_name='auth data')), 26 | ], 27 | options={ 28 | 'abstract': False, 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /service/templates/service/service_provider/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | Create new service 4 |

5 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /accounts/authenticate.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.backends import BaseBackend 2 | 3 | from accounts.models import Customer, ServiceProvider 4 | 5 | 6 | class PhoneNumberPasswordBackend(BaseBackend): 7 | def authenticate(self, request, phone_number=None, password=None): 8 | try: 9 | customer = Customer.objects.get(phone_number=phone_number) 10 | except Customer.DoesNotExist: 11 | return None 12 | else: 13 | if password: # if the password is sent via the form 14 | if customer.check_password(password): 15 | return customer 16 | else: 17 | return None 18 | return customer 19 | 20 | def get_user(self, user_id): 21 | try: 22 | customer = Customer.objects.get(pk=user_id) 23 | except Customer.DoesNotExist: 24 | return None 25 | else: 26 | return customer 27 | 28 | 29 | class ServiceProviderAuthentication(BaseBackend): 30 | def authenticate(self, request, username=None, password=None): 31 | try: 32 | user = ServiceProvider.objects.get(username=username, password=password) 33 | return user 34 | 35 | except ServiceProvider.DoesNotExist: 36 | return None 37 | 38 | def get_user(self, user_id): 39 | try: 40 | return ServiceProvider.objects.get(pk=user_id) 41 | except ServiceProvider.DoesNotExist: 42 | return None 43 | -------------------------------------------------------------------------------- /service/migrations/0005_auto_20210902_1522.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-02 10:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('service', '0004_alter_service_address'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='serviceavailabletime', 15 | name='available_time', 16 | ), 17 | migrations.AddField( 18 | model_name='serviceavailabletime', 19 | name='close_time', 20 | field=models.TimeField(null=True, verbose_name='close time'), 21 | ), 22 | migrations.AddField( 23 | model_name='serviceavailabletime', 24 | name='day', 25 | field=models.PositiveSmallIntegerField(choices=[(0, 'saturday'), (1, 'sunday'), (2, 'monday'), (3, 'tuesday'), (4, 'wednesday'), (5, 'thursday'), (6, 'friday')], null=True, verbose_name='day'), 26 | ), 27 | migrations.AddField( 28 | model_name='serviceavailabletime', 29 | name='is_close', 30 | field=models.BooleanField(blank=True, null=True, verbose_name='close day'), 31 | ), 32 | migrations.AddField( 33 | model_name='serviceavailabletime', 34 | name='open_time', 35 | field=models.TimeField(null=True, verbose_name='open time'), 36 | ), 37 | migrations.DeleteModel( 38 | name='AvailableTime', 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /order/migrations/0002_auto_20210913_1544.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-13 11:14 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('accounts', '0002_auto_20210831_0046'), 11 | ('payment', '0003_auto_20210912_1657'), 12 | ('order', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='order', 18 | options={'verbose_name': 'Order', 'verbose_name_plural': 'Orders'}, 19 | ), 20 | migrations.AddField( 21 | model_name='order', 22 | name='customer', 23 | field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='accounts.customer', verbose_name='customer'), 24 | preserve_default=False, 25 | ), 26 | migrations.AlterField( 27 | model_name='order', 28 | name='invoice', 29 | field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='order', to='payment.invoice', verbose_name='invoice'), 30 | ), 31 | migrations.AlterField( 32 | model_name='order', 33 | name='status', 34 | field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending')], default=0, verbose_name='status'), 35 | ), 36 | migrations.AlterModelTable( 37 | name='order', 38 | table='order', 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /order/templates/order/service/order_filter_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Payment Verification{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |
8 | {{ filter.form.as_p }} 9 | 10 |
11 |
12 |
13 | {% for order in orders %} 14 |
15 |
16 |
17 |
18 |

id : {{ order.id }}

19 |

status : {{ order.get_status_display }}

20 |

date time : {{ order.created_time }}

21 |

total price : {{ order.invoice.price }}$

22 |
23 |
24 |
25 | Show Factor 27 |
28 |
29 |
30 | {% endfor %} 31 |
32 |
33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /gateway/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext as _ 3 | 4 | from gateway.utils.zarrinpal import zarrinpal_request_handler, zarrinpal_payment_verify 5 | from library.models import BaseModel 6 | 7 | 8 | class Gateway(BaseModel): 9 | FUNCTION_ZARRINPAL = 'zarrinpal' 10 | FUNCTION_SAMAN = 'saman' 11 | GATEWAY_FUNCTIONS_CHOICES = ( 12 | (FUNCTION_ZARRINPAL, _('Zarrinpal')), 13 | (FUNCTION_SAMAN, _('Saman')) 14 | ) 15 | 16 | title = models.CharField(max_length=100, verbose_name=_('title')) 17 | gateway_request_url = models.CharField(max_length=150, verbose_name=_('request url'), blank=True) 18 | gateway_verify_url = models.CharField(max_length=150, verbose_name=_('verify url'), blank=True) 19 | gateway_code = models.CharField(max_length=20, verbose_name=_('gateway code'), choices=GATEWAY_FUNCTIONS_CHOICES) 20 | is_enable = models.BooleanField(verbose_name=_('is enable'), default=True) 21 | auth_data = models.TextField(verbose_name=_('auth data'), blank=True) 22 | 23 | def __str__(self): 24 | return self.title 25 | 26 | def get_request_handler(self): 27 | handlers = { 28 | self.FUNCTION_ZARRINPAL: zarrinpal_request_handler, 29 | self.FUNCTION_SAMAN: None, 30 | } 31 | return handlers[self.gateway_code] 32 | 33 | def get_verify_handler(self): 34 | handlers = { 35 | self.FUNCTION_ZARRINPAL: zarrinpal_payment_verify, 36 | self.FUNCTION_SAMAN: None, 37 | } 38 | return handlers[self.gateway_code] 39 | 40 | class Meta: 41 | verbose_name = _('Gateway') 42 | verbose_name_plural = _('Gateways') 43 | db_table = 'gateway' 44 | -------------------------------------------------------------------------------- /yummy/urls.py: -------------------------------------------------------------------------------- 1 | """yummy URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import path, include 20 | from django.views.generic import TemplateView 21 | 22 | urlpatterns = [ 23 | path('admin/', admin.site.urls, name='admin-panel'), 24 | path('home/', TemplateView.as_view(template_name='base.html'), name='home'), 25 | path('', TemplateView.as_view(template_name='base.html'), name='home'), 26 | path('accounts/', include('accounts.urls', namespace='accounts')), 27 | path('service/', include('service.urls', namespace='service')), 28 | path('address/', include('address.urls', namespace='address')), 29 | path('item/', include('item.urls', namespace='item')), 30 | path('cart/', include('cart.urls', namespace='cart')), 31 | path('payment/', include('payment.urls', namespace='payment')), 32 | path('order/', include('order.urls', namespace='order')), 33 | 34 | ] 35 | 36 | if settings.DEBUG: 37 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 38 | -------------------------------------------------------------------------------- /accounts/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from random import randint 3 | 4 | from django.contrib.auth.mixins import UserPassesTestMixin 5 | from django.core.exceptions import ValidationError 6 | 7 | from accounts.models import Customer, ServiceProvider 8 | 9 | 10 | def phone_number_validator(value): 11 | if not value.startswith('98') or len(value) != 12: 12 | raise ValidationError('phone number must be like 98912*******') 13 | 14 | 15 | def check_expire_time(request): 16 | try: 17 | expire_time = datetime.strptime(request.session['created-time'], '%Y-%m-%d %H:%M:%S') 18 | except KeyError: 19 | expire_time = None 20 | 21 | if expire_time: 22 | now = datetime.strptime(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S') 23 | if (now - expire_time) > timedelta(minutes=2): 24 | del request.session['code'] 25 | del request.session['created-time'] 26 | 27 | 28 | def set_phone_number_session(request, phone_number): 29 | request.session['phone_number'] = phone_number 30 | request.session['code'] = randint(1000, 9999) 31 | request.session['created_time'] = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) 32 | print(request.session['code']) 33 | print(request.session['created_time']) 34 | 35 | 36 | def check_is_not_authenticated(user): 37 | return not user.is_authenticated 38 | 39 | 40 | def can_set_password(user): 41 | return not user.password 42 | 43 | 44 | class IsCustomer(UserPassesTestMixin): 45 | raise_exception = True 46 | 47 | def test_func(self): 48 | return isinstance(self.request.user, Customer) 49 | 50 | 51 | class IsServiceProvider(UserPassesTestMixin): 52 | raise_exception = True 53 | 54 | def test_func(self): 55 | return isinstance(self.request.user, ServiceProvider) 56 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load accounts_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 13 | 14 | {% block title %} {% endblock %} 15 | {% block extra_css %}{% endblock %} 16 | 17 | 18 | 19 | {% is_customer request.user as customer_flag %} 20 | {% is_service_provider request.user as service_provider_flag %} 21 | {% is_admin_user request.user as admin_flag %} 22 | 23 | 24 | {% if service_provider_flag %} 25 | {% include 'inc/service_provider_navbar.html' %} 26 | {% elif customer_flag %} 27 | {% include 'inc/customer_navbar.html' %} 28 | {% elif admin_flag %} 29 | {% include 'inc/admin_navbar.html' %} 30 | {% else %} 31 | {% include 'inc/customer_navbar.html' %} 32 | {% endif %} 33 | {% block banner %}{% endblock %} 34 | 35 | 36 |
37 | {% if messages %} 38 | {% for message in messages %} 39 | 40 | {% endfor %} 41 | {% endif %} 42 | {% block content %} 43 | {% endblock %} 44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /service/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from service.models import Service, ServiceCategory, DeliveryArea, ServiceAvailableTime 4 | 5 | 6 | class ServiceCreateUpdateForm(forms.ModelForm): 7 | class Meta: 8 | model = Service 9 | fields = ('name', 'service_type', 'minimum_purchase', 'available', 'logo', 'banner') 10 | widgets = { 11 | 'name': forms.TextInput(attrs={'class': 'form-control'}), 12 | 'service_type': forms.Select(attrs={'class': 'form-control'}), 13 | 'minimum_purchase': forms.NumberInput(attrs={'class': 'form-control'}), 14 | 'logo': forms.FileInput(attrs={'class': 'form-control'}), 15 | 'banner': forms.FileInput(attrs={'class': 'form-control'}), 16 | 'available': forms.NullBooleanSelect(attrs={'class': 'form-control'}), 17 | } 18 | 19 | 20 | class ServiceCategoryCreateUpdateForm(forms.ModelForm): 21 | class Meta: 22 | model = ServiceCategory 23 | fields = ('name',) 24 | widgets = {'name': forms.TextInput(attrs={'class': 'form-control'})} 25 | 26 | 27 | class DeliveryAreaCreateUpdateForm(forms.ModelForm): 28 | class Meta: 29 | model = DeliveryArea 30 | fields = ('area',) 31 | widgets = {'area': forms.Select(attrs={'class': 'form-control'})} 32 | 33 | 34 | class ServiceAvailableTimeCreateUpdateForm(forms.ModelForm): 35 | class Meta: 36 | model = ServiceAvailableTime 37 | fields = ('day', 'open_time', 'close_time', 'is_close') 38 | widgets = { 39 | 'day': forms.Select(attrs={'class': 'form-control'}), 40 | 'open_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), 41 | 'close_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), 42 | 'is_close': forms.NullBooleanSelect(attrs={'class': 'form-control'}) 43 | } 44 | -------------------------------------------------------------------------------- /item/templates/item/service_provider_item_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

Category Detail

5 |
Category Name : {{ category.name }}
6 |
7 |
Items
8 |
9 | Add item 10 |

11 |
12 | {% for item in items %} 13 |
14 |
15 |
16 | {% if item.image %} 17 | {{ item.name }} image 20 | {% else %} 21 | 22 | {% endif %} 23 |
{{ item.name }}

24 | 25 |
Price : {{ item.price }}
26 |

{{ item.description | truncatechars:30 }}

27 | Detail 28 | Update 30 | Delete 32 |
33 |
34 |
35 | {% endfor %} 36 |
37 | 38 | {% endblock %} -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, reverse_lazy 2 | from django.contrib.auth.views import LogoutView 3 | 4 | from accounts.views import CustomerLoginRegisterView, CustomerPhoneNumberConfirmView, CustomerPasswordConfirmView, \ 5 | CustomerProfileView, CustomerSetPasswordView, ServiceProviderRegistrationView, ServiceProviderLoginView, \ 6 | ServiceProviderProfileView, CustomerProfileUpdateView, CustomerChangePasswordView, \ 7 | ServiceProviderChangePasswordView, CustomerLogoutView 8 | 9 | app_name = 'accounts' 10 | 11 | urlpatterns = [ 12 | path('customer/login-register/', CustomerLoginRegisterView.as_view(), name='customer-login-register'), 13 | path('customer/code-confirm/', CustomerPhoneNumberConfirmView.as_view(), name='customer-code-confirm'), 14 | path('customer/password-confirm/', CustomerPasswordConfirmView.as_view(), name='customer-password-confirm'), 15 | path('customer/profile/', CustomerProfileView.as_view(), name='customer-profile'), 16 | path('customer/profile-update/', CustomerProfileUpdateView.as_view(), name='customer-profile-update'), 17 | path('customer/set-password/', CustomerSetPasswordView.as_view(), name='customer-set-password'), 18 | path('customer/change-password/', CustomerChangePasswordView.as_view(), name='customer-change-password'), 19 | path('customer/logout/', CustomerLogoutView.as_view(), name='customer-logout'), 20 | path( 21 | 'serviceprovider/registration/', ServiceProviderRegistrationView.as_view(), name='service-provider-registration' 22 | ), 23 | path('serviceprovider/login/', ServiceProviderLoginView.as_view(), name='service-provider-login'), 24 | path('serviceprovider/profile/', ServiceProviderProfileView.as_view(), name='service-provider-profile'), 25 | path('serviceprovider/change-password/', ServiceProviderChangePasswordView.as_view(), 26 | name='service-provider-change-password'), 27 | path('serviceprovider/logout/', LogoutView.as_view(next_page=reverse_lazy('accounts:service-provider-login')), 28 | name='service-provider-logout'), 29 | ] 30 | -------------------------------------------------------------------------------- /templates/inc/customer_navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /order/templates/order/customer/order_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} Orders {% endblock %} 3 | 4 | {% block content %} 5 |

Active orders

6 | {% if active_orders %} 7 | 20 | {% else %} 21 |
22 |
You have no active orders :(
23 | {% endif %} 24 |

Delivered orders

25 | {% if delivered_orders %} 26 | 41 | {% else %} 42 |
43 |
You have no delivered orders
44 | {% endif %} 45 | {% endblock %} -------------------------------------------------------------------------------- /address/migrations/0002_auto_20210830_1305.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-30 08:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('address', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='CustomerAddress', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 18 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 19 | ('floor', models.SmallIntegerField(verbose_name='floor')), 20 | ('plaque', models.SmallIntegerField(verbose_name='plaque')), 21 | ], 22 | options={ 23 | 'verbose_name': 'CustomerAddress', 24 | 'verbose_name_plural': 'CustomerAddresses', 25 | 'db_table': 'customer_address', 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='ServiceAddress', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 33 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 34 | ('floor', models.SmallIntegerField(verbose_name='floor')), 35 | ('plaque', models.SmallIntegerField(verbose_name='plaque')), 36 | ], 37 | options={ 38 | 'verbose_name': 'ServiceAddress', 39 | 'verbose_name_plural': 'ServiceAddresses', 40 | 'db_table': 'service_address', 41 | }, 42 | ), 43 | migrations.RemoveField( 44 | model_name='address', 45 | name='area', 46 | ), 47 | migrations.RemoveField( 48 | model_name='address', 49 | name='city', 50 | ), 51 | migrations.RemoveField( 52 | model_name='address', 53 | name='state', 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /order/templates/order/service/order_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Order Detail{% endblock %} 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |
9 |

id : {{ order.id }}

10 |

status : {{ order.get_status_display }}

11 |

date time : {{ order.created_time }}

12 |

total price : {{ order.invoice.price }}$

13 |
14 |
15 | 16 |
17 |

Lines:

18 |
19 |
20 |
21 |

22 | Item 23 |

24 |
25 |
26 |

27 | Quantity 28 |

29 |
30 |
31 | {% for cartline in order.invoice.cart.lines.all %} 32 |
33 |
34 |
35 |

36 | {{ cartline.item }} 37 |

38 |
39 |
40 |

41 | {{ cartline.quantity }} 42 |

43 |
44 |
45 |
46 | {% endfor %} 47 |
48 |
49 | Change 51 | Status 52 | 53 |
54 |
55 | {% endblock %} -------------------------------------------------------------------------------- /payment/migrations/0003_auto_20210912_1657.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-12 12:27 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('address', '0006_rename_user_customeraddress_customer_user'), 11 | ('gateway', '0001_initial'), 12 | ('cart', '0002_remove_cart_invoice'), 13 | ('accounts', '0002_auto_20210831_0046'), 14 | ('payment', '0002_alter_invoice_address'), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='invoice', 20 | name='cart', 21 | field=models.OneToOneField(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='invoice', to='cart.cart', verbose_name='cart'), 22 | preserve_default=False, 23 | ), 24 | migrations.AddField( 25 | model_name='payment', 26 | name='authority', 27 | field=models.CharField(blank=True, max_length=64, verbose_name='authority'), 28 | ), 29 | migrations.AddField( 30 | model_name='payment', 31 | name='customer', 32 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='accounts.customer', verbose_name='customer'), 33 | ), 34 | migrations.AddField( 35 | model_name='payment', 36 | name='gateway', 37 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='gateway.gateway', verbose_name='gateway'), 38 | ), 39 | migrations.AddField( 40 | model_name='payment', 41 | name='payment_log', 42 | field=models.TextField(blank=True, verbose_name='logs'), 43 | ), 44 | migrations.AlterField( 45 | model_name='invoice', 46 | name='address', 47 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.customeraddress', verbose_name='address'), 48 | ), 49 | migrations.AlterField( 50 | model_name='payment', 51 | name='invoice', 52 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='payment.invoice', verbose_name='invoice'), 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /templates/inc/admin_navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /address/migrations/0003_auto_20210830_1305.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-30 08:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('service', '0002_alter_service_address'), 11 | ('address', '0002_auto_20210830_1305'), 12 | ('accounts', '0001_initial'), 13 | ('payment', '0002_alter_invoice_address'), 14 | ] 15 | 16 | operations = [ 17 | migrations.DeleteModel( 18 | name='Address', 19 | ), 20 | migrations.AddField( 21 | model_name='serviceaddress', 22 | name='area', 23 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.area', verbose_name='area'), 24 | ), 25 | migrations.AddField( 26 | model_name='serviceaddress', 27 | name='city', 28 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.city', verbose_name='city'), 29 | ), 30 | migrations.AddField( 31 | model_name='serviceaddress', 32 | name='state', 33 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.state', verbose_name='state'), 34 | ), 35 | migrations.AddField( 36 | model_name='customeraddress', 37 | name='area', 38 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.area', verbose_name='area'), 39 | ), 40 | migrations.AddField( 41 | model_name='customeraddress', 42 | name='city', 43 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.city', verbose_name='city'), 44 | ), 45 | migrations.AddField( 46 | model_name='customeraddress', 47 | name='state', 48 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.state', verbose_name='state'), 49 | ), 50 | migrations.AddField( 51 | model_name='customeraddress', 52 | name='user', 53 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='accounts.customer', verbose_name='customer'), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /item/models.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | from django.db import models 5 | from django.urls import reverse_lazy 6 | from django.utils.translation import gettext as _ 7 | 8 | from library.models import BaseModel 9 | from service.models import Service, ServiceCategory 10 | 11 | 12 | class ItemManager(models.Manager): 13 | def available(self): 14 | return self.get_queryset().filter(available=True) 15 | 16 | 17 | class Item(BaseModel): 18 | upc = models.BigIntegerField(verbose_name=_('upc'), unique=True, db_index=True, editable=False) 19 | available = models.BooleanField(verbose_name=_('available'), default=True) 20 | name = models.CharField(verbose_name=_('name'), max_length=50) 21 | description = models.TextField(verbose_name=_('description'), blank=True) 22 | price = models.IntegerField(verbose_name=_('price')) 23 | image = models.ImageField(verbose_name=_('image'), blank=True, null=True, upload_to='items/') 24 | service = models.ForeignKey(Service, verbose_name=_('service'), related_name='items', on_delete=models.CASCADE) 25 | category = models.ForeignKey(ServiceCategory, verbose_name=_('category'), related_name='items', 26 | on_delete=models.CASCADE) 27 | objects = ItemManager() 28 | 29 | class Meta: 30 | verbose_name = _('Item') 31 | verbose_name_plural = _('Items') 32 | db_table = 'item' 33 | 34 | @property 35 | def stock(self): 36 | return self.line.quantity 37 | 38 | def __str__(self): 39 | return self.name 40 | 41 | def save(self, force_insert=False, force_update=False, using=None, 42 | update_fields=None): 43 | if self.pk is None: # only when we the instance is created 44 | random_digits = ''.join(random.choice(string.digits) for _ in range(5)) 45 | self.upc = int(random_digits) 46 | return super().save(force_insert, force_update, using, update_fields) 47 | 48 | def get_absolute_url(self): 49 | return reverse_lazy("item:detail", kwargs={'pk': self.pk}) 50 | 51 | 52 | class ItemLine(BaseModel): 53 | item = models.OneToOneField(Item, verbose_name=_('item'), related_name='line', on_delete=models.CASCADE) 54 | quantity = models.PositiveIntegerField(default=0, verbose_name=_('quantity')) 55 | 56 | class Meta: 57 | verbose_name = _('Item line') 58 | verbose_name_plural = _('Item lines') 59 | db_table = 'item_line' 60 | 61 | def __str__(self): 62 | return f"{self.quantity}" 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | media/ 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | .idea/ 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # media 133 | media/ -------------------------------------------------------------------------------- /payment/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-28 20:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('accounts', '0001_initial'), 14 | ('address', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Invoice', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 23 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 24 | ('price', models.IntegerField(verbose_name='price')), 25 | ('is_paid', models.BooleanField(default=False, verbose_name='is paid')), 26 | ('address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.address', verbose_name='address')), 27 | ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='accounts.customer', verbose_name='customer')), 28 | ], 29 | options={ 30 | 'verbose_name': 'Invoice', 31 | 'verbose_name_plural': 'Invoices', 32 | 'db_table': 'invoice', 33 | }, 34 | ), 35 | migrations.CreateModel( 36 | name='Payment', 37 | fields=[ 38 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 40 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 41 | ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True, verbose_name='uuid')), 42 | ('price', models.IntegerField(verbose_name='price')), 43 | ('is_paid', models.BooleanField(default=False, verbose_name='is paid')), 44 | ('invoice', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='payment.invoice', verbose_name='invoice')), 45 | ], 46 | options={ 47 | 'verbose_name': 'Payment', 48 | 'verbose_name_plural': 'Payments', 49 | 'db_table': 'payment', 50 | }, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /templates/inc/service_provider_navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /item/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-28 20:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('service', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Item', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 21 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 22 | ('upc', models.BigIntegerField(db_index=True, unique=True, verbose_name='upc')), 23 | ('available', models.BooleanField(default=True, verbose_name='available')), 24 | ('name', models.CharField(max_length=50, verbose_name='name')), 25 | ('description', models.TextField(blank=True, verbose_name='description')), 26 | ('price', models.IntegerField(verbose_name='price')), 27 | ('image', models.ImageField(blank=True, null=True, upload_to='items/', verbose_name='image')), 28 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='service.servicecategory', verbose_name='category')), 29 | ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='service.service', verbose_name='service')), 30 | ], 31 | options={ 32 | 'verbose_name': 'Item', 33 | 'verbose_name_plural': 'Items', 34 | 'db_table': 'item', 35 | }, 36 | ), 37 | migrations.CreateModel( 38 | name='ItemLine', 39 | fields=[ 40 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 41 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 42 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 43 | ('quantity', models.PositiveIntegerField(default=0, verbose_name='quantity')), 44 | ('item', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='line', to='item.item', verbose_name='item')), 45 | ], 46 | options={ 47 | 'verbose_name': 'Item line', 48 | 'verbose_name_plural': 'Item lines', 49 | 'db_table': 'item_line', 50 | }, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /cart/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-09-07 12:46 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('payment', '0002_alter_invoice_address'), 13 | ('item', '0002_alter_item_upc'), 14 | ('accounts', '0002_auto_20210831_0046'), 15 | ('service', '0008_service_available'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Cart', 21 | fields=[ 22 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 24 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 25 | ('is_paid', models.BooleanField(default=False, verbose_name='is paid')), 26 | ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='carts', to='accounts.customer', verbose_name='customer')), 27 | ('invoice', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cart', to='payment.invoice', verbose_name='invoice')), 28 | ('service', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='carts', to='service.service', verbose_name='service')), 29 | ], 30 | options={ 31 | 'verbose_name': 'Cart', 32 | 'verbose_name_plural': 'Carts', 33 | 'db_table': 'cart', 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='CartLine', 38 | fields=[ 39 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 41 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 42 | ('quantity', models.PositiveIntegerField(default=1, verbose_name='quantity')), 43 | ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='cart.cart', verbose_name='cart')), 44 | ('item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='lines', to='item.item', verbose_name='item')), 45 | ], 46 | options={ 47 | 'verbose_name': 'Cart line', 48 | 'verbose_name_plural': 'Cart lines', 49 | 'db_table': 'cart_line', 50 | 'ordering': ('created_time', 'modified_time'), 51 | 'unique_together': {('item', 'cart')}, 52 | }, 53 | ), 54 | ] -------------------------------------------------------------------------------- /cart/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models, transaction 2 | from django.db.models import Sum, F 3 | from django.utils.translation import gettext as _ 4 | from accounts.models import Customer 5 | from item.models import Item 6 | from library.models import BaseModel 7 | from service.models import Service 8 | 9 | 10 | class Cart(BaseModel): 11 | customer = models.ForeignKey( 12 | Customer, 13 | verbose_name=_('customer'), 14 | null=True, blank=True, 15 | related_name='carts', 16 | on_delete=models.CASCADE 17 | ) 18 | is_paid = models.BooleanField(default=False, verbose_name=_('is paid')) 19 | service = models.ForeignKey( 20 | Service, 21 | verbose_name=_('service'), 22 | related_name='carts', 23 | on_delete=models.PROTECT, 24 | null=True 25 | ) 26 | 27 | def __str__(self): 28 | return f"{self.customer} - {'Paid' if self.is_paid else 'Not paid'}" 29 | 30 | @property 31 | def total_price(self): 32 | return self.lines.all().annotate(price=F('quantity') * F('item__price')).aggregate( 33 | total_price=Sum('price')).get('total_price') 34 | 35 | @classmethod 36 | def get_cart(cls, cart_id): 37 | if cart_id is None: 38 | cart = cls.objects.create() 39 | else: 40 | cart, created = cls.objects.get_or_create(pk=cart_id) 41 | return cart 42 | 43 | def create_or_increase(self, item): 44 | with transaction.atomic(): 45 | 46 | if item.service != self.service: # adding the first item or item with different service 47 | self.empty_cart() 48 | self.service = item.service 49 | self.save() 50 | 51 | cart_line, create = self.lines.select_for_update().get_or_create(item=item, defaults={'cart': self}) 52 | if not create: 53 | cart_line.quantity += 1 54 | cart_line.save() 55 | 56 | def empty_cart(self): 57 | self.lines.all().delete() 58 | 59 | class Meta: 60 | verbose_name = _('Cart') 61 | verbose_name_plural = _('Carts') 62 | db_table = 'cart' 63 | 64 | 65 | class CartLine(BaseModel): 66 | item = models.ForeignKey(Item, verbose_name=_('item'), related_name='lines', on_delete=models.PROTECT) 67 | cart = models.ForeignKey(Cart, verbose_name=_('cart'), related_name='lines', on_delete=models.CASCADE) 68 | quantity = models.PositiveIntegerField(default=1, verbose_name=_('quantity')) 69 | 70 | def __str__(self): 71 | return f"{self.item} - {self.quantity}" 72 | 73 | @property 74 | def price(self): 75 | return self.item.price * self.quantity 76 | 77 | class Meta: 78 | verbose_name = _('Cart line') 79 | verbose_name_plural = _('Cart lines') 80 | db_table = 'cart_line' 81 | ordering = ('created_time', 'modified_time') 82 | unique_together = ('item', 'cart') # each cart can have one cart line with the same item 83 | -------------------------------------------------------------------------------- /service/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ServiceProviderServiceCreateView, ServiceProviderServiceUpdateView, ServiceProviderServiceDeleteView, \ 4 | ServiceProviderServiceDetailView, ServiceProviderServiceListView, ServiceProviderServiceCategoryCreateView, \ 5 | ServiceProviderServiceCategoryUpdateView, ServiceProviderServiceCategoryDeleteView, \ 6 | ServiceProviderDeliveryAreaCreate, ServiceProviderDeliveryUpdateView, \ 7 | ServiceProviderDeliveryDeleteView, ServiceProviderServiceAvailableTimeCreateView, \ 8 | ServiceProviderServiceAvailableTimeUpdateView, ServiceProviderServiceAvailableTimeDeleteView, ServiceListView 9 | 10 | app_name = 'service' 11 | 12 | urlpatterns = ( 13 | path('serviceprovider/create/', ServiceProviderServiceCreateView.as_view(), 14 | name='service-provider-service-create'), 15 | path('serviceprovider/update/', ServiceProviderServiceUpdateView.as_view(), 16 | name='service-provider-service-update'), 17 | path('serviceprovider/delete/', ServiceProviderServiceDeleteView.as_view(), 18 | name='service-provider-service-delete'), 19 | path('serviceprovider/detail/', ServiceProviderServiceDetailView.as_view(), 20 | name='service-provider-service-detail'), 21 | path('serviceprovider/list/', ServiceProviderServiceListView.as_view(), name='service-provider-service-list'), 22 | 23 | path('serviceprovider/category/create//', ServiceProviderServiceCategoryCreateView.as_view(), 24 | name='service-provider-service-category-create'), 25 | path('serviceprovider/category/update//', ServiceProviderServiceCategoryUpdateView.as_view(), 26 | name='service-provider-service-category-update'), 27 | path('serviceprovider/category/delete//', ServiceProviderServiceCategoryDeleteView.as_view(), 28 | name='service-provider-service-category-delete'), 29 | 30 | path('serviceprovider/deliveryarea/create/', ServiceProviderDeliveryAreaCreate.as_view(), 31 | name='service-provider-delivery-area-create'), 32 | path('serviceprovider/deliveryarea/update/', ServiceProviderDeliveryUpdateView.as_view(), 33 | name='service-provider-delivery-area-update'), 34 | path('serviceprovider/deliveryarea/delete/', ServiceProviderDeliveryDeleteView.as_view(), 35 | name='service-provider-delivery-area-delete'), 36 | 37 | path( 38 | 'serviceprovider/availabletime/create/', 39 | ServiceProviderServiceAvailableTimeCreateView.as_view(), 40 | name='service-provider-available-time-create' 41 | ), 42 | path('serviceprovider/availabletime/update/', ServiceProviderServiceAvailableTimeUpdateView.as_view(), 43 | name='service-provider-available-time-update'), 44 | path('serviceprovider/availabletime/delete/', ServiceProviderServiceAvailableTimeDeleteView.as_view(), 45 | name='service-provider-available-time-delete'), 46 | 47 | path('list/', ServiceListView.as_view(), name='service-list') 48 | ) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yummy 2 | --- 3 | **This project is going to be a clone of sanpp food with Python and Django framework.** 4 | 5 | --- 6 | 7 | ## What is yummy? 8 | 9 | It’s actually a service which users can order food very easily and restaurant managers can register their services so 10 | that other people can use them. 11 | 12 | As I said this is going to be a clone of this service , clearly It’s not going to implement all the features 13 | 14 | --- 15 | 16 | ## The goal of this project: 17 | 18 | The main purpose of this project is being an acceptable resume and also a good practice of Django framework 19 | 20 | --- 21 | 22 | ## Features: 23 | 24 | - [x] We will have two type of users in this project : 1 - Customer 2 - Service Provider. 25 | - [x] Each service provider can provide different services such as (restaurant, fast food, confectionery, supermarket, 26 | …). 27 | - [x] Each service will be able to have a menu containing different items. 28 | - [x] Each service will be able to have custom categories for the items of the menu. 29 | - [x] Each service provider must specify the supported areas for the item delivery for the service. 30 | - [ ] Each service provider can add discount on some of their items for a limited time. 31 | - [x] Each service will have active days and hours. 32 | - [x] Each customer can have different addresses. 33 | - [x] Each customer will be able to see the services(only the supported services in their area). 34 | - [x] Each customer can add items to their cart(note : Each cart can only contain items from one specific service, that 35 | means adding items from different services causes multiple carts). 36 | - [ ] Each customer will be able to add comment for the items which was in their cart after the order was delivered( 37 | note: customers will be able to add one comment for an item after each successful order). 38 | - [ ] Each item will have a score based on its comments. 39 | - [x] Each user will be to see the status of the order after the payment has been successful. 40 | - [ ] The quantity of each item must be increased and decreased at successful orders. 41 | 42 | --- 43 | 44 | ## Architecture of the project: 45 | 46 | The project is based on the MVT architecture of the Django framework, so we will use SSR(server side rendering) 47 | 48 | --- 49 | ## How to run: 50 | 51 | 1- first create a virtualEnvirment 52 | ``` 53 | python3 -m venv .env 54 | // or other ways to create a VENV 55 | ``` 56 | 2- then activate it 57 | ``` 58 | source .env/bin/activate 59 | ``` 60 | 3- start to install requirments.txt file 61 | ``` 62 | pip install -r requirements.txt 63 | ``` 64 | note: 65 | if suds-jurko throw error please install it in this way 66 | ``` 67 | pip install -i https://m.devpi.net/jaraco/dev suds-jurko 68 | ``` 69 | then use 70 | ``` 71 | pip install -r requirements.txt 72 | ``` 73 | 4- on setting file you must change some places 74 | - you must set a DBMS and its settings(if you junior ad django use this [url](https://docs.djangoproject.com/en/3.2/ref/settings/#databases) 75 | - you must set ```ALLOWED_HOSTS,DEBUG``` in setting file 76 | 5- start migrate to ctreate tables and columns in your choosen database 77 | 6- enjoy it :) 78 | 79 | 80 | 81 | --- 82 | -------------------------------------------------------------------------------- /payment/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.db import transaction 3 | from django.http import HttpResponse, HttpResponseRedirect 4 | from django.shortcuts import render, get_object_or_404 5 | from django.urls import reverse_lazy 6 | from django.utils.decorators import method_decorator 7 | from django.views import View 8 | from django.views.decorators.http import require_http_methods 9 | 10 | from address.models import CustomerAddress 11 | from cart.models import Cart 12 | from library.utils import CustomUserPasses 13 | from payment.forms import AddressSelectForm, GatewaySelectForm 14 | from payment.models import Payment, Invoice 15 | 16 | 17 | @method_decorator(require_http_methods(['POST', 'GET']), name='dispatch') 18 | @method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') 19 | class CheckoutView(CustomUserPasses, View): 20 | 21 | def test_func(self): 22 | if not self.cart.lines.exists(): 23 | return False 24 | return True 25 | 26 | def dispatch(self, request, *args, **kwargs): 27 | cart_id = self.request.COOKIES.get('cart_id', None) 28 | self.cart = get_object_or_404(Cart, pk=cart_id) 29 | if self.cart.customer is None: 30 | self.cart.customer = request.user 31 | self.cart.save() 32 | 33 | return super().dispatch(request, *args, **kwargs) 34 | 35 | def get(self, request, *args, **kwargs): 36 | address_form = AddressSelectForm() 37 | address_form.base_fields['address'].queyset = CustomerAddress.objects.filter(customer_user=request.user) 38 | gateway_form = GatewaySelectForm() 39 | return render(request, 'payment/checkout.html', 40 | context={'address_form': address_form, 'gateway_form': gateway_form} 41 | ) 42 | 43 | def post(self, request, *args, **kwargs): 44 | address_form = AddressSelectForm(request.POST) 45 | gateway_form = GatewaySelectForm(request.POST) 46 | 47 | if address_form.is_valid() and gateway_form.is_valid(): 48 | address = address_form.cleaned_data['address'] 49 | gateway = gateway_form.cleaned_data['gateway'] 50 | with transaction.atomic(): 51 | payment = Invoice.create(request.user, self.cart, address=address, gateway=gateway) 52 | bank_url = payment.bank_page 53 | 54 | return HttpResponseRedirect(bank_url) 55 | return self.get(request, *args, **kwargs) 56 | 57 | 58 | @method_decorator(require_http_methods(['GET']), name='dispatch') 59 | @method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') 60 | class PaymentVerify(View): 61 | def get(self, request, *args, **kwargs): 62 | authority = self.request.GET.get('Authority', None) 63 | if authority: 64 | payment = get_object_or_404(Payment, authority=authority) 65 | is_paid, ref_id = payment.verify() 66 | response = render(request, 'payment/verify.html', {'is_paid': is_paid, 'ref_id': ref_id}) 67 | response.delete_cookie('cart_id') 68 | return response 69 | -------------------------------------------------------------------------------- /address/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from accounts.models import Customer 4 | from library.models import BaseModel 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | 8 | class State(BaseModel): 9 | name = models.CharField(max_length=20, verbose_name=_('name')) 10 | slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True) 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | class Meta: 16 | verbose_name = _('State') 17 | verbose_name_plural = _('States') 18 | db_table = 'state' 19 | 20 | 21 | class City(BaseModel): 22 | name = models.CharField(max_length=20, verbose_name=_('name')) 23 | slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True) 24 | state = models.ForeignKey(State, verbose_name=_('state'), related_name='cities', on_delete=models.CASCADE) 25 | 26 | def __str__(self): 27 | return f'{self.state} - {self.name}' 28 | 29 | class Meta: 30 | verbose_name = _('City') 31 | verbose_name_plural = _('Cities') 32 | db_table = 'city' 33 | 34 | 35 | class Area(BaseModel): 36 | name = models.CharField(max_length=20, verbose_name=_('name')) 37 | slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True) 38 | city = models.ForeignKey(City, verbose_name=_('city'), related_name='areas', on_delete=models.CASCADE) 39 | 40 | def __str__(self): 41 | return f'{self.city.name} - {self.name}' 42 | 43 | class Meta: 44 | verbose_name = _('Area') 45 | verbose_name_plural = _('Areas') 46 | db_table = 'area' 47 | 48 | 49 | class BaseAddress(BaseModel): 50 | street = models.CharField(max_length=50) 51 | alley = models.CharField(max_length=30) 52 | floor = models.SmallIntegerField(verbose_name=_('floor')) 53 | plaque = models.SmallIntegerField(verbose_name=_('plaque')) 54 | 55 | class Meta: 56 | abstract = True 57 | 58 | 59 | class CustomerAddress(BaseAddress): 60 | customer_user = models.ForeignKey( 61 | Customer, 62 | verbose_name=_('customer'), 63 | related_name='c_addresses', 64 | on_delete=models.CASCADE 65 | ) 66 | state = models.ForeignKey(State, verbose_name=_('state'), related_name='c_addresses', on_delete=models.CASCADE) 67 | city = models.ForeignKey(City, verbose_name=_('city'), related_name='c_addresses', on_delete=models.CASCADE) 68 | area = models.ForeignKey(Area, verbose_name=_('area'), related_name='c_addresses', on_delete=models.CASCADE) 69 | 70 | def __str__(self): 71 | return f'{self.customer_user} - {self.city} - {self.area}' 72 | 73 | class Meta: 74 | verbose_name = _('CustomerAddress') 75 | verbose_name_plural = _('CustomerAddresses') 76 | db_table = 'customer_address' 77 | 78 | 79 | class ServiceAddress(BaseAddress): 80 | state = models.ForeignKey(State, verbose_name=_('state'), related_name='s_addresses', on_delete=models.CASCADE) 81 | city = models.ForeignKey(City, verbose_name=_('city'), related_name='s_addresses', on_delete=models.CASCADE) 82 | area = models.ForeignKey(Area, verbose_name=_('area'), related_name='s_addresses', on_delete=models.CASCADE) 83 | 84 | def __str__(self): 85 | return f'{self.area.name}، {self.street}، {self.alley}' 86 | 87 | class Meta: 88 | verbose_name = _('ServiceAddress') 89 | verbose_name_plural = _('ServiceAddresses') 90 | db_table = 'service_address' 91 | -------------------------------------------------------------------------------- /service/templates/service/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% load service_delivary_area %} 4 | {% block content %} 5 |
6 |
7 |
8 |
10 |
11 | {{ filter.form.as_p }} 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | {% for service in services %} 20 | 21 |
22 |
23 |
24 |
25 | {% if service.banner %} 26 | 27 | {% else %} 28 | 29 | {% endif %} 30 |
31 |
32 | {% if service.logo %} 33 | 35 | {% else %} 36 | 37 | {% endif %} 38 |
39 |
40 |
41 |
{{ service.name.title }}
42 |
43 |

{{ service.address | truncatechars:30 }}

44 | {% delivery_area_string service as areas %} 45 |

{{ areas | truncatechars:15 }}

46 |

{{ service.areas | truncatechars:20 }}

47 | 48 |
49 | 50 |
51 | Menu 52 |
53 |
54 |
55 |
56 |
57 | {% endfor %} 58 |
59 |
60 |
61 | {% endblock %} -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-28 20:31 2 | 3 | import accounts.models 4 | import django.contrib.auth.models 5 | import django.contrib.auth.validators 6 | from django.db import migrations, models 7 | import django.utils.timezone 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Customer', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('phone_number', models.CharField(max_length=13, unique=True, verbose_name='phone number')), 25 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 26 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 27 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 28 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 29 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 30 | ], 31 | options={ 32 | 'verbose_name': 'Customer', 33 | 'verbose_name_plural': 'Customers', 34 | 'db_table': 'customer', 35 | }, 36 | managers=[ 37 | ('objects', accounts.models.CustomerManager()), 38 | ], 39 | ), 40 | migrations.CreateModel( 41 | name='ServiceProvider', 42 | fields=[ 43 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('password', models.CharField(max_length=128, verbose_name='password')), 45 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 46 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 47 | ('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')), 48 | ('phone_number', models.CharField(max_length=13, unique=True, verbose_name='phone number')), 49 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 50 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 51 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 52 | ], 53 | options={ 54 | 'verbose_name': 'Service provider', 55 | 'verbose_name_plural': 'Service providers', 56 | 'db_table': 'service_provider', 57 | }, 58 | managers=[ 59 | ('objects', django.contrib.auth.models.UserManager()), 60 | ], 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.base_user import AbstractBaseUser 2 | from django.contrib.auth.hashers import make_password 3 | from django.contrib.auth.models import PermissionsMixin, AbstractUser, UserManager 4 | from django.contrib.auth.validators import UnicodeUsernameValidator 5 | from django.db import models 6 | from django.utils import timezone 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | 10 | class CustomerManager(UserManager): 11 | def _create_user(self, phone_number, password, **extra_fields): 12 | if not phone_number: 13 | raise ValueError('The given phone_number must be set') 14 | 15 | user = self.model(phone_number=phone_number, **extra_fields) 16 | user.password = make_password(password) 17 | user.save(using=self._db) 18 | return user 19 | 20 | def create_user(self, phone_number, password=None, **extra_fields): 21 | extra_fields.setdefault('is_staff', False) 22 | return self._create_user(phone_number, password, **extra_fields) 23 | 24 | def create(self, phone_number, password=None, **extra_fields): 25 | return self.create_user(phone_number, password, **extra_fields) 26 | 27 | 28 | class Customer(AbstractBaseUser): 29 | phone_number = models.CharField(max_length=12, verbose_name=_('phone number'), unique=True) 30 | first_name = models.CharField(_('first name'), max_length=150, blank=True) 31 | last_name = models.CharField(_('last name'), max_length=150, blank=True) 32 | is_staff = models.BooleanField( 33 | _('staff status'), 34 | default=False, 35 | help_text=_('Designates whether the user can log into this admin site.'), 36 | ) 37 | is_active = models.BooleanField( 38 | _('active'), 39 | default=True, 40 | help_text=_( 41 | 'Designates whether this user should be treated as active. ' 42 | 'Unselect this instead of deleting accounts.' 43 | ), 44 | ) 45 | date_joined = models.DateTimeField(_('date joined'), default=timezone.now) 46 | 47 | USERNAME_FIELD = 'phone_number' 48 | REQUIRED_FIELDS = [] 49 | objects = CustomerManager() 50 | 51 | @property 52 | def full_name(self): 53 | full_name = '%s %s' % (self.first_name, self.last_name) 54 | return full_name.strip() 55 | 56 | def __str__(self): 57 | return f"{self.full_name} - {self.phone_number}" 58 | 59 | class Meta: 60 | verbose_name = _('Customer') 61 | verbose_name_plural = _('Customers') 62 | db_table = 'customer' 63 | 64 | 65 | class ServiceProvider(AbstractBaseUser): 66 | username_validator = UnicodeUsernameValidator() 67 | username = models.CharField( 68 | _('username'), 69 | max_length=150, 70 | unique=True, 71 | help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), 72 | validators=[username_validator], 73 | error_messages={ 74 | 'unique': _("A user with that username already exists."), 75 | }, 76 | ) 77 | email = models.EmailField(_('email address'), blank=True, unique=True) 78 | phone_number = models.CharField(max_length=12, verbose_name=_('phone number'), unique=True) 79 | is_staff = models.BooleanField( 80 | _('staff status'), 81 | default=False, 82 | help_text=_('Designates whether the user can log into this admin site.'), 83 | ) 84 | is_active = models.BooleanField( 85 | _('active'), 86 | default=True, 87 | help_text=_( 88 | 'Designates whether this user should be treated as active. ' 89 | 'Unselect this instead of deleting accounts.' 90 | ), 91 | ) 92 | date_joined = models.DateTimeField(_('date joined'), default=timezone.now) 93 | 94 | objects = UserManager() 95 | 96 | EMAIL_FIELD = 'email' 97 | USERNAME_FIELD = 'username' 98 | REQUIRED_FIELDS = ['email'] 99 | 100 | class Meta: 101 | verbose_name = _('Service provider') 102 | verbose_name_plural = _('Service providers') 103 | db_table = 'service_provider' 104 | -------------------------------------------------------------------------------- /cart/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect, Http404 2 | from django.shortcuts import get_object_or_404 3 | from django.urls import reverse_lazy 4 | from django.utils.decorators import method_decorator 5 | from django.views.decorators.http import require_http_methods 6 | from django.views.generic import View 7 | 8 | from accounts.models import Customer 9 | from cart.models import Cart, CartLine 10 | from item.models import Item 11 | 12 | 13 | class BaseCartView(View): 14 | def check_customer(self, cart): 15 | user = self.request.user 16 | if user.is_authenticated: 17 | if not isinstance(user, Customer): 18 | raise Http404 19 | if cart.customer is None: 20 | cart.customer = user 21 | cart.save() 22 | return cart 23 | else: 24 | if user != cart.customer: 25 | raise Http404 26 | return cart 27 | elif cart.customer is not None: 28 | return Cart.objects.create() 29 | else: 30 | return cart 31 | 32 | def get_cart_set_cookie(self, redirect_url=None): 33 | response = HttpResponseRedirect(redirect_url) 34 | cart = Cart.get_cart(self.request.COOKIES.get('cart_id', None)) 35 | cart = self.check_customer(cart) 36 | response.set_cookie('cart_id', cart.pk, max_age=15 * 60) 37 | return cart, response 38 | 39 | 40 | @method_decorator(require_http_methods(('POST',)), name='dispatch') 41 | class AddToCartView(BaseCartView): 42 | 43 | def setup(self, request, *args, **kwargs): 44 | super().setup(request, *args, **kwargs) 45 | self.kwargs['item'] = get_object_or_404(Item, pk=request.POST['item_id']) 46 | 47 | def get_success_url(self): 48 | return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['item'].service.pk}) 49 | 50 | def post(self, request, *args, **kwargs): 51 | cart, response = self.get_cart_set_cookie(self.get_success_url()) 52 | cart.create_or_increase(self.kwargs['item']) 53 | return response 54 | 55 | 56 | @method_decorator(require_http_methods(('POST',)), name='dispatch') 57 | class CartLineDeleteView(BaseCartView): 58 | model = CartLine 59 | 60 | def get_success_url(self): 61 | return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['cart_line'].item.service.pk}) 62 | 63 | def setup(self, request, *args, **kwargs): 64 | super().setup(request, *args, **kwargs) 65 | self.kwargs['cart_line'] = get_object_or_404(CartLine, pk=self.kwargs['pk']) 66 | 67 | def post(self, request, *args, **kwargs): 68 | cart, response = self.get_cart_set_cookie(self.get_success_url()) 69 | if self.kwargs['cart_line'].cart != cart: 70 | raise Http404 71 | self.kwargs['cart_line'].delete() 72 | return response 73 | 74 | 75 | @method_decorator(require_http_methods(('POST',)), name='dispatch') 76 | class CartLineDecreaseView(BaseCartView): 77 | 78 | def setup(self, request, *args, **kwargs): 79 | super().setup(request, *args, **kwargs) 80 | self.kwargs['cart_line'] = get_object_or_404(CartLine, id=self.kwargs['pk']) 81 | 82 | def get_success_url(self): 83 | return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['cart_line'].item.service.pk}) 84 | 85 | def post(self, request, *args, **kwargs): 86 | cart, response = self.get_cart_set_cookie(self.get_success_url()) 87 | if self.kwargs['cart_line'].cart != cart: 88 | raise Http404 89 | 90 | if self.kwargs['cart_line'].quantity >= 2: 91 | self.kwargs['cart_line'].quantity -= 1 92 | self.kwargs['cart_line'].save() 93 | 94 | return response 95 | 96 | 97 | @method_decorator(require_http_methods(('POST',)), name='dispatch') 98 | class EmptyCartView(BaseCartView): 99 | 100 | def post(self, request, *args, **kwargs): 101 | cart = Cart.get_cart(self.request.COOKIES.get('cart_id', None)) 102 | cart = self.check_customer(cart) 103 | cart.empty_cart() 104 | cart.save() 105 | response = HttpResponseRedirect(reverse_lazy('item:list', kwargs={'service_pk': cart.service.pk})) 106 | return response 107 | -------------------------------------------------------------------------------- /address/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-28 20:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='State', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 20 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 21 | ('name', models.CharField(max_length=20, verbose_name='name')), 22 | ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')), 23 | ], 24 | options={ 25 | 'verbose_name': 'State', 26 | 'verbose_name_plural': 'States', 27 | 'db_table': 'state', 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='City', 32 | fields=[ 33 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 35 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 36 | ('name', models.CharField(max_length=20, verbose_name='name')), 37 | ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')), 38 | ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cities', to='address.state', verbose_name='state')), 39 | ], 40 | options={ 41 | 'verbose_name': 'City', 42 | 'verbose_name_plural': 'Cities', 43 | 'db_table': 'city', 44 | }, 45 | ), 46 | migrations.CreateModel( 47 | name='Area', 48 | fields=[ 49 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 50 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 51 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 52 | ('name', models.CharField(max_length=20, verbose_name='name')), 53 | ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')), 54 | ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='areas', to='address.city', verbose_name='city')), 55 | ], 56 | options={ 57 | 'verbose_name': 'Area', 58 | 'verbose_name_plural': 'Areas', 59 | 'db_table': 'area', 60 | }, 61 | ), 62 | migrations.CreateModel( 63 | name='Address', 64 | fields=[ 65 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 66 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 67 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 68 | ('floor', models.SmallIntegerField(verbose_name='floor')), 69 | ('plaque', models.SmallIntegerField(verbose_name='plaque')), 70 | ('area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.area', verbose_name='area')), 71 | ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.city', verbose_name='city')), 72 | ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.state', verbose_name='state')), 73 | ], 74 | options={ 75 | 'verbose_name': 'Address', 76 | 'verbose_name_plural': 'Addresses', 77 | 'db_table': 'address', 78 | }, 79 | ), 80 | ] 81 | -------------------------------------------------------------------------------- /yummy/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for yummy project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | from yummy.local_settings import * 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | 24 | # Application definition 25 | 26 | INSTALLED_APPS = [ 27 | 'django.contrib.admin', 28 | 'django.contrib.auth', 29 | 'django.contrib.contenttypes', 30 | 'django.contrib.sessions', 31 | 'django.contrib.messages', 32 | 'django.contrib.staticfiles', 33 | 34 | 'django_filters', 35 | 36 | 'service.apps.ServiceConfig', 37 | 'order.apps.OrderConfig', 38 | 'address.apps.AddressConfig', 39 | 'accounts.apps.AccountsConfig', 40 | 'item.apps.ItemConfig', 41 | 'cart.apps.CartConfig', 42 | 'payment.apps.PaymentConfig', 43 | 'gateway.apps.GatewayConfig', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | ROOT_URLCONF = 'yummy.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [os.path.join(BASE_DIR, 'templates/')], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = 'yummy.wsgi.application' 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.postgresql', 82 | 'NAME': DB_NAME, 83 | 'USER': DB_USER, 84 | 'PASSWORD': DB_PASSWORD, 85 | 'HOST': DB_HOST, 86 | 'PORT': DB_PORT, 87 | } 88 | } 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'en-us' 112 | 113 | TIME_ZONE = 'Asia/Tehran' 114 | 115 | USE_I18N = True 116 | 117 | USE_L10N = True 118 | 119 | USE_TZ = True 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 123 | 124 | STATIC_URL = '/static/' 125 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 126 | 127 | MEDIA_URL = '/media/' 128 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 129 | 130 | # Default primary key field type 131 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 132 | 133 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 134 | 135 | AUTHENTICATION_BACKENDS = [ 136 | 'django.contrib.auth.backends.ModelBackend', 137 | 'accounts.authenticate.PhoneNumberPasswordBackend', 138 | 'accounts.authenticate.ServiceProviderAuthentication', 139 | ] 140 | 141 | CALL_BACK = 'http://127.0.0.1:8000/payment/verify/' 142 | -------------------------------------------------------------------------------- /payment/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models, transaction 2 | from django.utils.translation import gettext as _ 3 | from accounts.models import Customer 4 | from address.models import CustomerAddress 5 | from cart.models import Cart 6 | from gateway.models import Gateway 7 | from library.models import BaseModel 8 | import uuid 9 | 10 | from yummy.settings import CALL_BACK 11 | 12 | 13 | class Invoice(BaseModel): 14 | customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='invoices', 15 | on_delete=models.SET_NULL, null=True) 16 | price = models.IntegerField(verbose_name=_('price')) 17 | is_paid = models.BooleanField(verbose_name=_('is paid'), default=False) 18 | cart = models.OneToOneField(Cart, verbose_name=_('cart'), related_name='invoice', on_delete=models.PROTECT) 19 | address = models.ForeignKey(CustomerAddress, verbose_name=_('address'), related_name='invoices', 20 | on_delete=models.SET_NULL, null=True) 21 | 22 | class Meta: 23 | verbose_name = _('Invoice') 24 | verbose_name_plural = _('Invoices') 25 | db_table = 'invoice' 26 | 27 | @classmethod 28 | def create_payment(cls, invoice, gateway): 29 | return Payment.objects.create(invoice=invoice, price=invoice.price, customer=invoice.customer, gateway=gateway) 30 | 31 | @classmethod 32 | def create(cls, user, cart, address, gateway): 33 | with transaction.atomic(): 34 | invoice = cls.objects.create(customer=user, cart=cart, price=cart.total_price, address=address) 35 | payment = cls.create_payment(invoice=invoice, gateway=gateway) 36 | 37 | return payment 38 | 39 | def __str__(self): 40 | return f"{self.customer} - {self.price} - {'Paid' if self.is_paid else 'Not paid'}" 41 | 42 | 43 | class Payment(BaseModel): 44 | uuid = models.UUIDField(unique=True, verbose_name=_('uuid'), db_index=True, default=uuid.uuid4) 45 | invoice = models.ForeignKey(Invoice, verbose_name=_('invoice'), related_name='payment', on_delete=models.CASCADE) 46 | price = models.IntegerField(verbose_name=_('price')) 47 | customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='payments', 48 | on_delete=models.SET_NULL, null=True) 49 | is_paid = models.BooleanField(verbose_name=_('is paid'), default=False) 50 | gateway = models.ForeignKey(Gateway, verbose_name=_('gateway'), related_name='payments', on_delete=models.SET_NULL, 51 | null=True) 52 | payment_log = models.TextField(verbose_name=_('logs'), blank=True) 53 | authority = models.CharField(max_length=64, verbose_name=_('authority'), blank=True) 54 | 55 | class Meta: 56 | verbose_name = _('Payment') 57 | verbose_name_plural = _('Payments') 58 | db_table = 'payment' 59 | 60 | def __str__(self): 61 | return f"{self.price} - {'Paid' if self.is_paid else 'Not paid'}" 62 | 63 | def get_request_handler_data(self): 64 | return dict( 65 | amount=self.price, description=self.description, user_email=getattr(self.customer, 'email', None), 66 | user_phone_number=self.customer.phone_number, REQUEST_URL=self.gateway.gateway_request_url, 67 | MERCHANT_ID=self.gateway.auth_data, CALL_BACK=CALL_BACK, 68 | ) 69 | 70 | @property 71 | def bank_page(self): 72 | handler = self.gateway.get_request_handler() 73 | if handler: 74 | link, authority = handler(**self.get_request_handler_data()) 75 | if authority: 76 | self.authority = authority 77 | self.save() 78 | return link 79 | 80 | def get_verify_handler_data(self): 81 | return dict( 82 | amount=self.price, authority=self.authority, REQUEST_URL=self.gateway.gateway_request_url, 83 | MERCHANT_ID=self.gateway.auth_data 84 | ) 85 | 86 | def verify(self): 87 | from order.models import Order 88 | handler = self.gateway.get_verify_handler() 89 | if handler: 90 | is_paid, ref_id = handler(**self.get_verify_handler_data()) 91 | if is_paid: 92 | with transaction.atomic(): 93 | self.is_paid = True 94 | self.invoice.is_paid = True 95 | self.invoice.cart.is_paid = True 96 | self.invoice.cart.save() 97 | self.invoice.save() 98 | self.save() 99 | Order.create(invoice=self.invoice) 100 | return is_paid, ref_id 101 | 102 | @property 103 | def description(self): 104 | return 'buy of snapp food service' 105 | -------------------------------------------------------------------------------- /service/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.text import slugify 3 | 4 | from accounts.models import ServiceProvider 5 | from library.models import BaseModel 6 | from django.utils.translation import ugettext_lazy as _ 7 | from address.models import ServiceAddress, Area 8 | import uuid 9 | 10 | 11 | class Service(BaseModel): 12 | RESTAURANT = 0 13 | CAFE = 1 14 | CONFECTIONERY = 2 15 | SUPERMARKET = 3 16 | 17 | SERVICE_TYPES = ( 18 | (RESTAURANT, _('restaurant')), 19 | (CAFE, _('cafe')), 20 | (CONFECTIONERY, _('confectionery')), 21 | (SUPERMARKET, _('supermarket')), 22 | ) 23 | 24 | uuid = models.UUIDField(default=uuid.uuid4, verbose_name=_('uuid'), unique=True, db_index=True) 25 | service_provider = models.ForeignKey( 26 | ServiceProvider, 27 | verbose_name=_('service provider'), 28 | related_name='services', 29 | on_delete=models.CASCADE 30 | ) 31 | name = models.CharField(max_length=40, verbose_name=_('name')) 32 | service_type = models.PositiveSmallIntegerField(verbose_name=_('service type'), choices=SERVICE_TYPES) 33 | minimum_purchase = models.DecimalField(max_digits=9, decimal_places=0, verbose_name=_('minimum purchase')) 34 | available = models.BooleanField(default=False, verbose_name=_('available')) 35 | logo = models.ImageField(verbose_name=_('logo'), upload_to='service/logos/', null=True, blank=True) 36 | banner = models.ImageField(verbose_name=_('banner'), upload_to='service/banners/', null=True, blank=True) 37 | 38 | address = models.OneToOneField( 39 | ServiceAddress, 40 | verbose_name=_('address'), 41 | related_name='services', 42 | on_delete=models.SET_NULL, 43 | null=True, 44 | blank=True 45 | ) 46 | 47 | def __str__(self): 48 | return f'{self.name} - {self.get_service_type_display()}' 49 | 50 | class Meta: 51 | verbose_name = _('Service') 52 | verbose_name_plural = _('Services') 53 | db_table = 'service' 54 | ordering = ('created_time',) 55 | 56 | 57 | class ServiceCategory(BaseModel): 58 | name = models.CharField(max_length=30, verbose_name=_('name')) 59 | slug = models.SlugField(max_length=35, verbose_name=_('slug'), allow_unicode=True) 60 | service = models.ForeignKey(Service, verbose_name=_('service'), related_name='categories', on_delete=models.CASCADE) 61 | 62 | def __str__(self): 63 | return f'{self.name} - {self.service.name}' 64 | 65 | def save(self, *args, **kwargs): 66 | self.slug = slugify(self.name, allow_unicode=True) 67 | super().save(*args, **kwargs) 68 | 69 | class Meta: 70 | verbose_name = _('ServiceCategory') 71 | verbose_name_plural = _('ServiceCategories') 72 | db_table = 'service_category' 73 | 74 | 75 | class DeliveryArea(BaseModel): 76 | service = models.ForeignKey( 77 | Service, verbose_name=_('service'), related_name='delivery_areas', on_delete=models.CASCADE 78 | ) 79 | area = models.ForeignKey(Area, verbose_name=_('area'), related_name='delivery_areas', on_delete=models.CASCADE) 80 | 81 | def __str__(self): 82 | return f'{self.service.name} - {self.area}' 83 | 84 | class Meta: 85 | verbose_name = _('DeliveryArea') 86 | verbose_name_plural = _('DeliveryAreas') 87 | db_table = 'delivery_area' 88 | 89 | 90 | class ServiceAvailableTime(BaseModel): 91 | SAT_DAY = 0 92 | SUN_DAY = 1 93 | MON_DAY = 2 94 | TUE_DAY = 3 95 | WED_DAY = 4 96 | THU_DAY = 5 97 | FRI_DAY = 6 98 | 99 | DAYS = ( 100 | (SAT_DAY, _('saturday')), 101 | (SUN_DAY, _('sunday')), 102 | (MON_DAY, _('monday')), 103 | (TUE_DAY, _('tuesday')), 104 | (WED_DAY, _('wednesday')), 105 | (THU_DAY, _('thursday')), 106 | (FRI_DAY, _('friday')) 107 | ) 108 | service = models.ForeignKey( 109 | Service, verbose_name=_('service'), related_name='available_times', on_delete=models.CASCADE 110 | ) 111 | day = models.PositiveSmallIntegerField(verbose_name=_('day'), choices=DAYS, null=True) 112 | open_time = models.TimeField(verbose_name=_('open time'), null=True) 113 | close_time = models.TimeField(verbose_name=_('close time'), null=True) 114 | is_close = models.BooleanField(verbose_name=_('close day'), blank=True, null=True) 115 | 116 | def __str__(self): 117 | return f'{self.service.name} - {self.get_day_display()} - ' \ 118 | f'{self.is_close if self.close_time else self.open_time and "-" and self.close_time}' 119 | 120 | class Meta: 121 | verbose_name = _('ServiceAvailableTime') 122 | verbose_name_plural = _('ServiceAvailableTimes') 123 | db_table = 'service_available_time' 124 | ordering = ('day',) 125 | -------------------------------------------------------------------------------- /address/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.db import transaction 3 | from django.shortcuts import get_object_or_404 4 | from django.urls import reverse_lazy 5 | from django.utils.decorators import method_decorator 6 | from django.views.generic import CreateView, UpdateView, DeleteView, ListView 7 | 8 | from accounts.models import ServiceProvider 9 | from accounts.utils import IsCustomer, IsServiceProvider 10 | from address.forms import CustomerAddressCreateUpdateForm, ServiceAddressCreateUpdateForm 11 | from address.models import CustomerAddress, ServiceAddress 12 | from library.utils import CustomUserPasses 13 | from service.models import Service 14 | 15 | 16 | class BaseAddress: 17 | model = CustomerAddress 18 | form_class = CustomerAddressCreateUpdateForm 19 | template_name = 'address/create_update_form.html' 20 | 21 | 22 | @method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') 23 | class CustomerAddressCreateView(BaseAddress, IsCustomer, CreateView): 24 | success_url = reverse_lazy('accounts:customer-profile') 25 | 26 | def form_valid(self, form): 27 | instance = form.save(commit=False) 28 | instance.customer_user = self.request.user 29 | instance.save() 30 | return super().form_valid(form) 31 | 32 | 33 | @method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') 34 | class CustomerAddressUpdateView(BaseAddress, IsCustomer, UpdateView): 35 | success_url = reverse_lazy('address:customer-address-list') 36 | 37 | def test_func(self): 38 | result = super().test_func() 39 | obj = self.get_object() 40 | return result and obj.customer_user == self.request.user 41 | 42 | def form_valid(self, form): 43 | instance = form.save(commit=False) 44 | instance.user = self.request.user 45 | instance.save() 46 | return super().form_valid(form) 47 | 48 | 49 | @method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') 50 | class CustomerAddressDeleteView(IsCustomer, DeleteView): 51 | model = CustomerAddress 52 | template_name = 'address/delete_form.html' 53 | success_url = reverse_lazy('accounts:customer-profile') 54 | 55 | def test_func(self): 56 | result = super().test_func() 57 | obj = self.get_object() 58 | return result and obj.customer_user == self.request.user 59 | 60 | 61 | @method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') 62 | class CustomerAddressListView(ListView): 63 | model = CustomerAddress 64 | template_name = 'address/customer_address_list.html' 65 | context_object_name = 'addresses' 66 | 67 | def get_queryset(self): 68 | return super().get_queryset().filter(customer_user=self.request.user) 69 | 70 | 71 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 72 | class ServiceAddressCreateView(CustomUserPasses, CreateView): 73 | model = ServiceAddress 74 | form_class = ServiceAddressCreateUpdateForm 75 | template_name = 'address/create_update_form.html' 76 | success_url = reverse_lazy('accounts:service-provider-profile') 77 | raise_exception = True 78 | 79 | def setup(self, request, *args, **kwargs): 80 | super().setup(request, *args, **kwargs) 81 | self.service = get_object_or_404(Service, id=self.kwargs['service_pk']) 82 | 83 | def get_context_data(self, **kwargs): 84 | context = super().get_context_data(**kwargs) 85 | context['service'] = self.service 86 | return context 87 | 88 | def test_func(self): 89 | if not isinstance(self.request.user, ServiceProvider): 90 | return False 91 | if self.service.service_provider != self.request.user: 92 | return False 93 | if self.service.address: 94 | return False, True, reverse_lazy('accounts:service-provider-profile') 95 | return True 96 | 97 | def form_valid(self, form): 98 | with transaction.atomic(): 99 | instance = form.save() 100 | self.service.address = instance 101 | self.service.save() 102 | return super().form_valid(form) 103 | 104 | 105 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 106 | class ServiceAddressUpdateView(IsServiceProvider, UpdateView): 107 | model = ServiceAddress 108 | form_class = ServiceAddressCreateUpdateForm 109 | template_name = 'address/create_update_form.html' 110 | success_url = reverse_lazy('accounts:service-provider-profile') 111 | 112 | def test_func(self): 113 | result = super().test_func() 114 | obj = self.get_object() 115 | return result and obj.services.service_provider == self.request.user 116 | -------------------------------------------------------------------------------- /order/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.http import HttpResponseForbidden 3 | from django.shortcuts import get_object_or_404 4 | from django.urls import reverse_lazy 5 | from django.utils import timezone 6 | from django.utils.decorators import method_decorator 7 | from django.views.decorators.http import require_http_methods 8 | from django.views.generic import ListView, DetailView, UpdateView 9 | 10 | from django_filters.views import FilterView 11 | 12 | from order.filters import OrderFilter 13 | from order.models import Order 14 | from service.models import Service 15 | 16 | from library.utils import CustomUserPasses 17 | 18 | from accounts.models import Customer, ServiceProvider 19 | from accounts.utils import IsCustomer 20 | 21 | 22 | class BaseOrderServiceList(CustomUserPasses): 23 | model = Order 24 | 25 | def test_func(self): 26 | if not isinstance(self.request.user, ServiceProvider): 27 | return False 28 | if self.service.service_provider != self.request.user: 29 | return False 30 | return True 31 | 32 | 33 | class BaseOrderDetailUpdate(CustomUserPasses): 34 | model = Order 35 | 36 | def test_func(self): 37 | order = self.get_object() 38 | if not isinstance(self.reqeust.user, ServiceProvider): 39 | return False 40 | if order.invoice.cart.service.service_provider != self.request.user: 41 | return False 42 | return True 43 | 44 | 45 | @method_decorator(require_http_methods(['GET']), name='dispatch') 46 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 47 | class OrderServiceListFilterView(FilterView): 48 | context_object_name = 'orders' 49 | filterset_class = OrderFilter 50 | template_name = 'order/service/order_filter_list.html' 51 | 52 | def dispatch(self, request, *args, **kwargs): 53 | self.service = get_object_or_404(Service, pk=self.kwargs.get('service_pk', None)) 54 | if self.service.service_provider != self.request.user: 55 | return HttpResponseForbidden() 56 | return super().dispatch(request, *args, **kwargs) 57 | 58 | def get_queryset(self): 59 | return Order.objects.filter(invoice__cart__service=self.service) 60 | 61 | 62 | @method_decorator(require_http_methods(['GET']), name='dispatch') 63 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 64 | class OrderServiceListView(BaseOrderServiceList, ListView): 65 | context_object_name = 'orders' 66 | template_name = 'order/service/order_list.html' 67 | 68 | def dispatch(self, request, *args, **kwargs): 69 | self.service = get_object_or_404(Service, pk=self.kwargs.get('service_pk', None)) 70 | return super().dispatch(request, *args, **kwargs) 71 | 72 | def get_queryset(self): 73 | today = timezone.now() 74 | time_filter = dict( 75 | created_time__day=today.strftime('%d'), 76 | created_time__month=today.strftime('%m'), 77 | created_time__year=today.strftime('%Y') 78 | ) 79 | return Order.objects.exclude(is_delivered=True).filter(invoice__cart__service=self.service, **time_filter) 80 | 81 | 82 | @method_decorator(require_http_methods(['GET']), name='dispatch') 83 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 84 | class OrderServiceDetailView(BaseOrderDetailUpdate, DetailView): 85 | context_object_name = 'order' 86 | template_name = 'order/service/order_detail.html' 87 | 88 | 89 | @method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') 90 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 91 | class OrderServiceUpdateView(BaseOrderDetailUpdate, UpdateView): 92 | fields = ('status',) 93 | context_object_name = 'order' 94 | template_name = 'order/service/order_update.html' 95 | 96 | def get_success_url(self): 97 | order = self.get_object() 98 | return reverse_lazy('order:service-order-list', kwargs={'service_pk': order.invoice.cart.service.id}) 99 | 100 | 101 | @method_decorator(require_http_methods(['GET']), name='dispatch') 102 | @method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') 103 | class CustomerOrdersListView(IsCustomer, ListView): 104 | template_name = 'order/customer/order_list.html' 105 | context_object_name = 'active_orders' 106 | 107 | def get_queryset(self): 108 | return Order.objects.select_related('invoice__cart').filter(customer=self.request.user, status__in=(0, 1)) 109 | 110 | def get_context_data(self, *, object_list=None, **kwargs): 111 | context = super().get_context_data(object_list=None, **kwargs) 112 | context['delivered_orders'] = Order.objects.select_related('invoice__cart').filter(customer=self.request.user, 113 | status=2) 114 | return context 115 | 116 | 117 | @method_decorator(require_http_methods(['GET']), name='dispatch') 118 | @method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') 119 | class CustomerOrderDetailView(CustomUserPasses, DetailView): 120 | model = Order 121 | template_name = 'order/customer/order_detail.html' 122 | context_object_name = 'order' 123 | 124 | def test_func(self): 125 | user = self.request.user 126 | if not isinstance(user, Customer): 127 | return False 128 | if self.get_object().customer != self.request.user: 129 | return False 130 | return True 131 | -------------------------------------------------------------------------------- /service/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-08-28 20:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('accounts', '0001_initial'), 14 | ('address', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='AvailableTime', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 23 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 24 | ('day', models.PositiveSmallIntegerField(choices=[(0, 'saturday'), (1, 'sunday'), (2, 'monday'), (3, 'tuesday'), (4, 'wednesday'), (5, 'thursday'), (6, 'friday')], verbose_name='day')), 25 | ('open_time', models.DateTimeField(verbose_name='open time')), 26 | ('close_time', models.DateTimeField(verbose_name='close time')), 27 | ('close_day', models.BooleanField(blank=True, null=True, verbose_name='close day')), 28 | ], 29 | options={ 30 | 'verbose_name': 'AvailableTime', 31 | 'verbose_name_plural': 'AvailableTimes', 32 | 'db_table': 'available_time', 33 | }, 34 | ), 35 | migrations.CreateModel( 36 | name='Service', 37 | fields=[ 38 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 40 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 41 | ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True, verbose_name='uuid')), 42 | ('name', models.CharField(max_length=40, verbose_name='name')), 43 | ('service_type', models.PositiveSmallIntegerField(choices=[(0, 'restaurant'), (1, 'cafe'), (2, 'confectionery'), (3, 'supermarket')], verbose_name='service type')), 44 | ('minimum_purchase', models.DecimalField(decimal_places=0, max_digits=9, verbose_name='minimum purchase')), 45 | ('address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.address', verbose_name='address')), 46 | ('service_provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='accounts.serviceprovider', verbose_name='service provider')), 47 | ], 48 | options={ 49 | 'verbose_name': 'Service', 50 | 'verbose_name_plural': 'Services', 51 | 'db_table': 'service', 52 | }, 53 | ), 54 | migrations.CreateModel( 55 | name='ServiceCategory', 56 | fields=[ 57 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 58 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 59 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 60 | ('name', models.CharField(max_length=30, verbose_name='name')), 61 | ('slug', models.SlugField(allow_unicode=True, max_length=35, verbose_name='slug')), 62 | ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='service.service', verbose_name='service')), 63 | ], 64 | options={ 65 | 'verbose_name': 'ServiceCategory', 66 | 'verbose_name_plural': 'ServiceCategories', 67 | 'db_table': 'service_category', 68 | }, 69 | ), 70 | migrations.CreateModel( 71 | name='ServiceAvailableTime', 72 | fields=[ 73 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 74 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 75 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 76 | ('available_time', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='service.availabletime', verbose_name='available time')), 77 | ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='available_times', to='service.service', verbose_name='service')), 78 | ], 79 | options={ 80 | 'verbose_name': 'ServiceAvailableTime', 81 | 'verbose_name_plural': 'ServiceAvailableTimes', 82 | 'db_table': 'service_available_time', 83 | }, 84 | ), 85 | migrations.CreateModel( 86 | name='DeliveryArea', 87 | fields=[ 88 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 89 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), 90 | ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), 91 | ('area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='delivery_areas', to='address.area', verbose_name='area')), 92 | ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='delivery_areas', to='service.service', verbose_name='service')), 93 | ], 94 | options={ 95 | 'verbose_name': 'DeliveryArea', 96 | 'verbose_name_plural': 'DeliveryAreas', 97 | 'db_table': 'delivery_area', 98 | }, 99 | ), 100 | ] 101 | -------------------------------------------------------------------------------- /service/templates/service/service_provider/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |

Service Detail

5 |

Service name : {{ service.name }}

6 |

Service minimum purchase : {{ service.minimum_purchase }}

7 |

Service minimum purchase : {{ service.get_service_type_display }}

8 | {% if service.available %} 9 |

Service available : Yes

10 | {% else %} 11 |

Service available : No

12 | {% endif %} 13 | {% if service.logo %} 14 |

Service Logo Image :

15 | {% else %} 16 |

Service Logo Image : Not Yet Uploaded

17 | {% endif %} 18 | {% if service.banner %} 19 |

Service Banner Image :

20 | {% else %} 21 |

Service Logo Image : Not Yet Uploaded

22 | {% endif %} 23 | 24 |
25 |
26 |
27 |

Service Address

28 | {% if service.address %} 29 |

state : {{ service.address.state.name }}

30 |

city : {{ service.address.city.name }}

31 |

area : {{ service.address.area.name }}

32 |

street : {{ service.address.street }}

33 |

alley : {{ service.address.alley }}

34 |

floor : {{ service.address.floor }}

35 |

plaque : {{ service.address.plaque }}

36 | Update Address 37 | {% else %} 38 |

Your service yet not has address!Create Address

40 | {% endif %} 41 |
42 |
43 |
44 |

Service Categories

45 |

46 | Create new category 47 |

48 |
    49 | {% for category in service.categories.all %} 50 |
  • 51 | {{ category.name }} 52 |
    53 | Update 56 | Detail 59 | Delete 61 |
    62 |
  • 63 | {% endfor %} 64 |
65 |
66 |
67 |
68 |

Service Delivery Area

69 |
70 |
71 | Create new delivery area 72 |
73 |
74 | {% with deliveries=service.delivery_areas.all %} 75 | {% if deliveries %} 76 | {% for delivery in deliveries %} 77 |
  • 78 | {{ delivery.area.name }} 79 |
    80 | Update 82 | Delete 84 |
    85 |
  • 86 | {% endfor %} 87 | 88 | {% endif %} 89 | {% endwith %} 90 |
    91 |
    92 |
    93 |

    Service Available Time

    94 |
    95 |
    96 | Create new available 97 | time 98 |
    99 |
    100 | {% with availabletimes=service.available_times.all %} 101 | {% if availabletimes %} 102 | {% for availabletime in availabletimes %} 103 |
  • 104 | {{ availabletime.get_day_display }} | open at : {{ availabletime.open_time }} | close at 105 | : {{ availabletime.close_time }} | is close : {{ availabletime.is_close }} 106 |
    107 | Update 109 | Delete 111 |
    112 |
  • 113 | {% endfor %} 114 | 115 | {% endif %} 116 | {% endwith %} 117 |
    118 |
    119 | {% endblock %} 120 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth import password_validation 3 | from django.core.exceptions import ValidationError 4 | from django.db.models import Q 5 | from django.core.validators import int_list_validator 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | from accounts.models import ServiceProvider, Customer 9 | from accounts.utils import phone_number_validator 10 | 11 | 12 | class CustomerLoginRegisterForm(forms.Form): 13 | phone_number = forms.CharField(max_length=12, 14 | validators=[ 15 | int_list_validator(message=_('only digits are accepted')), 16 | phone_number_validator 17 | ], 18 | widget=forms.TextInput( 19 | attrs={'class': 'form-control', 'placeholder': 'phone number'}) 20 | ) 21 | 22 | 23 | class CustomerCodeConfirmForm(forms.Form): 24 | code = forms.CharField( 25 | validators=[int_list_validator(message=_('only digits are accepted'))], 26 | widget=forms.TextInput( 27 | attrs={ 28 | 'class': 'form-control', 29 | 'placeholder': _('confirmation code') 30 | } 31 | ) 32 | ) 33 | 34 | 35 | class CustomerPasswordForm(forms.Form): 36 | password = forms.CharField( 37 | widget=forms.PasswordInput( 38 | attrs={ 39 | 'class': 'form-control', 40 | 'placeholder': _('password') 41 | } 42 | ) 43 | ) 44 | 45 | 46 | class CustomerPasswordSetForm(forms.ModelForm): 47 | error_messages = { 48 | 'password_mismatch': _('The two password fields didn’t match.'), 49 | } 50 | password = forms.CharField( 51 | label=_("Password"), 52 | strip=False, 53 | widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), 54 | help_text=password_validation.password_validators_help_text_html(), 55 | ) 56 | password2 = forms.CharField( 57 | label=_("Password confirmation"), 58 | widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), 59 | strip=False, 60 | help_text=_("Enter the same password as before, for verification."), 61 | ) 62 | 63 | class Meta: 64 | model = Customer 65 | fields = ('password',) 66 | 67 | def clean_password2(self): 68 | password = self.cleaned_data.get("password") 69 | password2 = self.cleaned_data.get("password2") 70 | if password and password2 and password != password2: 71 | raise ValidationError( 72 | self.error_messages['password_mismatch'], 73 | code='password_mismatch', 74 | ) 75 | return password2 76 | 77 | def save(self, commit=True): 78 | customer = super().save(commit=False) 79 | customer.set_password(self.cleaned_data["password"]) 80 | if commit: 81 | customer.save() 82 | return customer 83 | 84 | 85 | class CustomerProfileUpdateForm(forms.ModelForm): 86 | class Meta: 87 | model = Customer 88 | fields = ('first_name', 'last_name') 89 | 90 | 91 | class ServiceProviderRegistrationForm(forms.ModelForm): 92 | username = forms.CharField( 93 | max_length=30, 94 | min_length=4, 95 | widget=forms.TextInput( 96 | attrs={ 97 | 'placeholder': 'Username', 98 | 'class': 'form-control'} 99 | ) 100 | ) 101 | phone_number = forms.CharField(max_length=12, 102 | validators=[ 103 | int_list_validator(message=_('only digits are accepted')), 104 | phone_number_validator 105 | ], 106 | widget=forms.TextInput( 107 | attrs={'class': 'form-control', 'placeholder': 'phone number'}) 108 | ) 109 | confirm_password = forms.CharField( 110 | widget=forms.PasswordInput( 111 | attrs={ 112 | 'placeholder': 'Confirm Password', 113 | 'class': 'form-control'} 114 | ) 115 | ) 116 | 117 | class Meta: 118 | model = ServiceProvider 119 | fields = ('username', 'email', 'phone_number', 'password', 'confirm_password') 120 | widgets = { 121 | 'email': forms.EmailInput(attrs={'placeholder': 'Email', 'class': 'form-control'}), 122 | 'phone_number': forms.TextInput(attrs={'placeholder': 'Phone Number', 'class': 'form-control'}), 123 | 'password': forms.PasswordInput(attrs={'placeholder': 'Password', 'class': 'form-control'}), 124 | } 125 | 126 | def clean_confirm_password(self): 127 | if self.cleaned_data['password'] != self.cleaned_data['confirm_password']: 128 | raise ValidationError('passwords not equal!') 129 | return self.cleaned_data['confirm_password'] 130 | 131 | 132 | class ServiceProviderLoginForm(forms.Form): 133 | username = forms.CharField( 134 | min_length=4, 135 | widget=forms.TextInput( 136 | attrs={ 137 | 'placeholder': 'Username, Email, Phone Number', 138 | 'class': 'form-control'} 139 | ) 140 | ) 141 | 142 | password = forms.CharField( 143 | max_length=30, 144 | min_length=4, 145 | widget=forms.PasswordInput( 146 | attrs={ 147 | 'placeholder': 'Password', 148 | 'class': 'form-control'} 149 | ) 150 | ) 151 | 152 | def clean(self): 153 | cleaned_data = super().clean() 154 | 155 | username = cleaned_data['username'] 156 | user = ServiceProvider.objects.filter( 157 | Q(username=username) | 158 | Q(email=username) | 159 | Q(phone_number=username), 160 | ).first() 161 | 162 | if user and user.check_password(cleaned_data['password']): 163 | cleaned_data['user'] = user 164 | return cleaned_data 165 | 166 | raise ValidationError('username or password invalid!') 167 | -------------------------------------------------------------------------------- /item/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import get_object_or_404 3 | from django.urls import reverse_lazy 4 | from django.utils.decorators import method_decorator 5 | from django.views.generic import FormView, UpdateView, DetailView, DeleteView, ListView 6 | 7 | from accounts.utils import IsServiceProvider 8 | from item.forms import ItemCreateForm, ItemUpdateForm 9 | from item.models import Item, ItemLine 10 | from service.models import Service, ServiceCategory 11 | 12 | 13 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 14 | class ItemCreateView(IsServiceProvider, FormView): 15 | model = Item 16 | form_class = ItemCreateForm 17 | template_name = 'item/create_form.html' 18 | 19 | def setup(self, request, *args, **kwargs): 20 | super().setup(request, *args, **kwargs) 21 | self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk']) 22 | self.kwargs['category'] = get_object_or_404(ServiceCategory, id=self.kwargs['category_pk']) 23 | 24 | def form_valid(self, form): 25 | item = form.save(commit=False) 26 | item.service = self.kwargs['service'] 27 | item.category = self.kwargs['category'] 28 | item.save() 29 | ItemLine.objects.create(item=item, quantity=form.cleaned_data['quantity']) 30 | item.save() 31 | return super().form_valid(form) 32 | 33 | def get_success_url(self): 34 | return reverse_lazy('item:service-provider-list', 35 | kwargs={'service_pk': self.kwargs['service'].pk, 'category_pk': self.kwargs['category'].pk} 36 | ) 37 | 38 | def test_func(self): 39 | result = super().test_func() 40 | service_check = self.kwargs['service'].service_provider == self.request.user 41 | return result and service_check 42 | 43 | 44 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 45 | class ItemUpdateView(IsServiceProvider, UpdateView): 46 | model = Item 47 | template_name = 'item/update_form.html' 48 | form_class = ItemUpdateForm 49 | 50 | def get_initial(self): 51 | initial = super().get_initial() 52 | initial['quantity'] = self.get_object().stock 53 | return initial 54 | 55 | def get_form_class(self): 56 | form = super().get_form_class() 57 | queryset = form.base_fields['category'].queryset 58 | form.base_fields['category'].queryset = queryset.filter(service=self.get_object().service) 59 | return form 60 | 61 | def form_valid(self, form): 62 | item = form.save(commit=False) 63 | item.line.quantity = form.cleaned_data['quantity'] 64 | item.line.save() 65 | item.save() 66 | return super().form_valid(form) 67 | 68 | def get_success_url(self): 69 | return reverse_lazy('item:detail-service-provider', kwargs={'pk': self.object.pk}) 70 | 71 | def test_func(self): 72 | result = super().test_func() 73 | return result and self.get_object().service.service_provider == self.request.user 74 | 75 | 76 | class ItemDetailView(DetailView): 77 | model = Item 78 | template_name = 'item/detail.html' 79 | context_object_name = 'item' 80 | 81 | def get_queryset(self): 82 | return Item.objects.available() 83 | 84 | 85 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 86 | class ServiceProviderItemDetailView(IsServiceProvider, DetailView): 87 | model = Item 88 | template_name = 'item/service_provider_detail.html' 89 | context_object_name = 'item' 90 | 91 | def test_func(self): 92 | result = super().test_func() 93 | return result and self.get_object().service.service_provider == self.request.user 94 | 95 | 96 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 97 | class ItemDeleteView(IsServiceProvider, DeleteView): 98 | model = Item 99 | template_name = 'item/delete.html' 100 | 101 | def test_func(self): 102 | result = super().test_func() 103 | return result and self.get_object().service.service_provider == self.request.user 104 | 105 | def get_success_url(self): 106 | return reverse_lazy('item:service-provider-list', 107 | kwargs={'service_pk': self.object.service.pk, 'category_pk': self.object.category.pk} 108 | ) 109 | 110 | 111 | class BaseItemListView(ListView): 112 | model = Item 113 | context_object_name = 'items' 114 | 115 | 116 | class ItemListView(BaseItemListView): 117 | template_name = 'item/item_list.html' 118 | 119 | def setup(self, request, *args, **kwargs): 120 | super().setup(request, *args, **kwargs) 121 | self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk']) 122 | 123 | def get_queryset(self): 124 | return Item.objects.available().filter(service=self.kwargs['service']) 125 | 126 | def get_context_data(self, *, object_list=None, **kwargs): 127 | context = super().get_context_data(object_list=None, **kwargs) 128 | context['service'] = self.kwargs['service'] 129 | return context 130 | 131 | 132 | @method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') 133 | class ServiceCategoryItemListView(IsServiceProvider, BaseItemListView): 134 | """ 135 | This view is for service-provider panel 136 | """ 137 | template_name = 'item/service_provider_item_list.html' 138 | 139 | def setup(self, request, *args, **kwargs): 140 | super().setup(request, *args, **kwargs) 141 | self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk']) 142 | self.kwargs['category'] = get_object_or_404(ServiceCategory, id=self.kwargs['category_pk'], 143 | service=self.kwargs['service']) 144 | 145 | def get_queryset(self): 146 | return Item.objects.filter(service=self.kwargs['service'], category=self.kwargs['category']) 147 | 148 | def test_func(self): 149 | result = super().test_func() 150 | service_check = self.kwargs['service'].service_provider == self.request.user 151 | return result and service_check 152 | 153 | def get_context_data(self, *, object_list=None, **kwargs): 154 | context = super().get_context_data(object_list=None, **kwargs) 155 | context['category'] = self.kwargs['category'] 156 | return context 157 | --------------------------------------------------------------------------------