diff --git a/.gitignore b/.gitignore
index b6e4761..0604bfd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
__pycache__/
*.py[cod]
*$py.class
-
+media/
# C extensions
*.so
@@ -54,6 +54,7 @@ coverage.xml
# Translations
*.mo
*.pot
+.idea/
# Django stuff:
*.log
@@ -127,3 +128,6 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# media
+media/
\ No newline at end of file
diff --git a/README.md b/README.md
index 8b13789..29fd154 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,48 @@
+Yummy
+---
+**This project is going to be a clone of sanpp food with Python and Django framework.**
+---
+
+## What is yummy?
+
+It’s actually a service which users can order food very easily and restaurant managers can register their services so
+that other people can use them.
+
+As I said this is going to be a clone of this service , clearly It’s not going to implement all the features
+
+---
+
+## The goal of this project:
+
+The main purpose of this project is being an acceptable resume and also a good practice of Django framework
+
+---
+
+## Features:
+
+- [x] We will have two type of users in this project : 1 - Customer 2 - Service Provider.
+- [x] Each service provider can provide different services such as (restaurant, fast food, confectionery, supermarket,
+ …).
+- [x] Each service will be able to have a menu containing different items.
+- [x] Each service will be able to have custom categories for the items of the menu.
+- [x] Each service provider must specify the supported areas for the item delivery for the service.
+- [ ] Each service provider can add discount on some of their items for a limited time.
+- [x] Each service will have active days and hours.
+- [x] Each customer can have different addresses.
+- [x] Each customer will be able to see the services(only the supported services in their area).
+- [x] Each customer can add items to their cart(note : Each cart can only contain items from one specific service, that
+ means adding items from different services causes multiple carts).
+- [ ] Each customer will be able to add comment for the items which was in their cart after the order was delivered(
+ note: customers will be able to add one comment for an item after each successful order).
+- [ ] Each item will have a score based on its comments.
+- [x] Each user will be to see the status of the order after the payment has been successful.
+- [ ] The quantity of each item must be increased and decreased at successful orders.
+
+---
+
+## Architecture of the project:
+
+The project is based on the MVT architecture of the Django framework, so we will use SSR(server side rendering)
+
+---
diff --git a/accounts/__init__.py b/accounts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/accounts/admin.py b/accounts/admin.py
new file mode 100644
index 0000000..c87f9ef
--- /dev/null
+++ b/accounts/admin.py
@@ -0,0 +1,22 @@
+from django.contrib import admin
+from django.contrib.auth.hashers import make_password
+
+from accounts.models import Customer, ServiceProvider
+
+
+@admin.register(Customer)
+class CustomerAdmin(admin.ModelAdmin):
+ list_display = ('phone_number', 'first_name', 'last_name', 'date_joined', 'is_active')
+ list_filter = ('is_active', 'date_joined')
+ search_fields = ('phone_number',)
+
+ def save_model(self, request, obj, form, change):
+ obj.password = make_password(form.cleaned_data['password'])
+ return super().save_model(request, obj, form, change)
+
+
+@admin.register(ServiceProvider)
+class ServiceProviderAdmin(admin.ModelAdmin):
+ list_display = ('username', 'email', 'phone_number', 'date_joined', 'is_active')
+ list_filter = ('is_active', 'date_joined')
+ search_fields = ('username', 'phone_number')
diff --git a/accounts/apps.py b/accounts/apps.py
new file mode 100644
index 0000000..3e3c765
--- /dev/null
+++ b/accounts/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'accounts'
diff --git a/accounts/authenticate.py b/accounts/authenticate.py
new file mode 100644
index 0000000..c350947
--- /dev/null
+++ b/accounts/authenticate.py
@@ -0,0 +1,42 @@
+from django.contrib.auth.backends import BaseBackend
+
+from accounts.models import Customer, ServiceProvider
+
+
+class PhoneNumberPasswordBackend(BaseBackend):
+ def authenticate(self, request, phone_number=None, password=None):
+ try:
+ customer = Customer.objects.get(phone_number=phone_number)
+ except Customer.DoesNotExist:
+ return None
+ else:
+ if password: # if the password is sent via the form
+ if customer.check_password(password):
+ return customer
+ else:
+ return None
+ return customer
+
+ def get_user(self, user_id):
+ try:
+ customer = Customer.objects.get(pk=user_id)
+ except Customer.DoesNotExist:
+ return None
+ else:
+ return customer
+
+
+class ServiceProviderAuthentication(BaseBackend):
+ def authenticate(self, request, username=None, password=None):
+ try:
+ user = ServiceProvider.objects.get(username=username, password=password)
+ return user
+
+ except ServiceProvider.DoesNotExist:
+ return None
+
+ def get_user(self, user_id):
+ try:
+ return ServiceProvider.objects.get(pk=user_id)
+ except ServiceProvider.DoesNotExist:
+ return None
diff --git a/accounts/forms.py b/accounts/forms.py
new file mode 100644
index 0000000..e351ced
--- /dev/null
+++ b/accounts/forms.py
@@ -0,0 +1,166 @@
+from django import forms
+from django.contrib.auth import password_validation
+from django.core.exceptions import ValidationError
+from django.db.models import Q
+from django.core.validators import int_list_validator
+from django.utils.translation import gettext_lazy as _
+
+from accounts.models import ServiceProvider, Customer
+from accounts.utils import phone_number_validator
+
+
+class CustomerLoginRegisterForm(forms.Form):
+ phone_number = forms.CharField(max_length=12,
+ validators=[
+ int_list_validator(message=_('only digits are accepted')),
+ phone_number_validator
+ ],
+ widget=forms.TextInput(
+ attrs={'class': 'form-control', 'placeholder': 'phone number'})
+ )
+
+
+class CustomerCodeConfirmForm(forms.Form):
+ code = forms.CharField(
+ validators=[int_list_validator(message=_('only digits are accepted'))],
+ widget=forms.TextInput(
+ attrs={
+ 'class': 'form-control',
+ 'placeholder': _('confirmation code')
+ }
+ )
+ )
+
+
+class CustomerPasswordForm(forms.Form):
+ password = forms.CharField(
+ widget=forms.PasswordInput(
+ attrs={
+ 'class': 'form-control',
+ 'placeholder': _('password')
+ }
+ )
+ )
+
+
+class CustomerPasswordSetForm(forms.ModelForm):
+ error_messages = {
+ 'password_mismatch': _('The two password fields didn’t match.'),
+ }
+ password = forms.CharField(
+ label=_("Password"),
+ strip=False,
+ widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
+ help_text=password_validation.password_validators_help_text_html(),
+ )
+ password2 = forms.CharField(
+ label=_("Password confirmation"),
+ widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
+ strip=False,
+ help_text=_("Enter the same password as before, for verification."),
+ )
+
+ class Meta:
+ model = Customer
+ fields = ('password',)
+
+ def clean_password2(self):
+ password = self.cleaned_data.get("password")
+ password2 = self.cleaned_data.get("password2")
+ if password and password2 and password != password2:
+ raise ValidationError(
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
+ return password2
+
+ def save(self, commit=True):
+ customer = super().save(commit=False)
+ customer.set_password(self.cleaned_data["password"])
+ if commit:
+ customer.save()
+ return customer
+
+
+class CustomerProfileUpdateForm(forms.ModelForm):
+ class Meta:
+ model = Customer
+ fields = ('first_name', 'last_name')
+
+
+class ServiceProviderRegistrationForm(forms.ModelForm):
+ username = forms.CharField(
+ max_length=30,
+ min_length=4,
+ widget=forms.TextInput(
+ attrs={
+ 'placeholder': 'Username',
+ 'class': 'form-control'}
+ )
+ )
+ phone_number = forms.CharField(max_length=12,
+ validators=[
+ int_list_validator(message=_('only digits are accepted')),
+ phone_number_validator
+ ],
+ widget=forms.TextInput(
+ attrs={'class': 'form-control', 'placeholder': 'phone number'})
+ )
+ confirm_password = forms.CharField(
+ widget=forms.PasswordInput(
+ attrs={
+ 'placeholder': 'Confirm Password',
+ 'class': 'form-control'}
+ )
+ )
+
+ class Meta:
+ model = ServiceProvider
+ fields = ('username', 'email', 'phone_number', 'password', 'confirm_password')
+ widgets = {
+ 'email': forms.EmailInput(attrs={'placeholder': 'Email', 'class': 'form-control'}),
+ 'phone_number': forms.TextInput(attrs={'placeholder': 'Phone Number', 'class': 'form-control'}),
+ 'password': forms.PasswordInput(attrs={'placeholder': 'Password', 'class': 'form-control'}),
+ }
+
+ def clean_confirm_password(self):
+ if self.cleaned_data['password'] != self.cleaned_data['confirm_password']:
+ raise ValidationError('passwords not equal!')
+ return self.cleaned_data['confirm_password']
+
+
+class ServiceProviderLoginForm(forms.Form):
+ username = forms.CharField(
+ min_length=4,
+ widget=forms.TextInput(
+ attrs={
+ 'placeholder': 'Username, Email, Phone Number',
+ 'class': 'form-control'}
+ )
+ )
+
+ password = forms.CharField(
+ max_length=30,
+ min_length=4,
+ widget=forms.PasswordInput(
+ attrs={
+ 'placeholder': 'Password',
+ 'class': 'form-control'}
+ )
+ )
+
+ def clean(self):
+ cleaned_data = super().clean()
+
+ username = cleaned_data['username']
+ user = ServiceProvider.objects.filter(
+ Q(username=username) |
+ Q(email=username) |
+ Q(phone_number=username),
+ ).first()
+
+ if user and user.check_password(cleaned_data['password']):
+ cleaned_data['user'] = user
+ return cleaned_data
+
+ raise ValidationError('username or password invalid!')
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000..6a7d712
--- /dev/null
+++ b/accounts/migrations/0001_initial.py
@@ -0,0 +1,62 @@
+# Generated by Django 3.2 on 2021-08-28 20:31
+
+import accounts.models
+import django.contrib.auth.models
+import django.contrib.auth.validators
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Customer',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('phone_number', models.CharField(max_length=13, unique=True, verbose_name='phone number')),
+ ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('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')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ ],
+ options={
+ 'verbose_name': 'Customer',
+ 'verbose_name_plural': 'Customers',
+ 'db_table': 'customer',
+ },
+ managers=[
+ ('objects', accounts.models.CustomerManager()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ServiceProvider',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('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')),
+ ('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')),
+ ('phone_number', models.CharField(max_length=13, unique=True, verbose_name='phone number')),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('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')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ ],
+ options={
+ 'verbose_name': 'Service provider',
+ 'verbose_name_plural': 'Service providers',
+ 'db_table': 'service_provider',
+ },
+ managers=[
+ ('objects', django.contrib.auth.models.UserManager()),
+ ],
+ ),
+ ]
diff --git a/accounts/migrations/0002_auto_20210831_0046.py b/accounts/migrations/0002_auto_20210831_0046.py
new file mode 100644
index 0000000..1c7f550
--- /dev/null
+++ b/accounts/migrations/0002_auto_20210831_0046.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2 on 2021-08-30 20:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='customer',
+ name='phone_number',
+ field=models.CharField(max_length=12, unique=True, verbose_name='phone number'),
+ ),
+ migrations.AlterField(
+ model_name='serviceprovider',
+ name='phone_number',
+ field=models.CharField(max_length=12, unique=True, verbose_name='phone number'),
+ ),
+ ]
diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/accounts/models.py b/accounts/models.py
new file mode 100644
index 0000000..5ef1952
--- /dev/null
+++ b/accounts/models.py
@@ -0,0 +1,103 @@
+from django.contrib.auth.base_user import AbstractBaseUser
+from django.contrib.auth.hashers import make_password
+from django.contrib.auth.models import PermissionsMixin, AbstractUser, UserManager
+from django.contrib.auth.validators import UnicodeUsernameValidator
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+
+class CustomerManager(UserManager):
+ def _create_user(self, phone_number, password, **extra_fields):
+ if not phone_number:
+ raise ValueError('The given phone_number must be set')
+
+ user = self.model(phone_number=phone_number, **extra_fields)
+ user.password = make_password(password)
+ user.save(using=self._db)
+ return user
+
+ def create_user(self, phone_number, password=None, **extra_fields):
+ extra_fields.setdefault('is_staff', False)
+ return self._create_user(phone_number, password, **extra_fields)
+
+ def create(self, phone_number, password=None, **extra_fields):
+ return self.create_user(phone_number, password, **extra_fields)
+
+
+class Customer(AbstractBaseUser):
+ phone_number = models.CharField(max_length=12, verbose_name=_('phone number'), unique=True)
+ first_name = models.CharField(_('first name'), max_length=150, blank=True)
+ last_name = models.CharField(_('last name'), max_length=150, blank=True)
+ is_staff = models.BooleanField(
+ _('staff status'),
+ default=False,
+ help_text=_('Designates whether the user can log into this admin site.'),
+ )
+ is_active = models.BooleanField(
+ _('active'),
+ default=True,
+ help_text=_(
+ 'Designates whether this user should be treated as active. '
+ 'Unselect this instead of deleting accounts.'
+ ),
+ )
+ date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
+
+ USERNAME_FIELD = 'phone_number'
+ REQUIRED_FIELDS = []
+ objects = CustomerManager()
+
+ @property
+ def full_name(self):
+ full_name = '%s %s' % (self.first_name, self.last_name)
+ return full_name.strip()
+
+ def __str__(self):
+ return f"{self.full_name} - {self.phone_number}"
+
+ class Meta:
+ verbose_name = _('Customer')
+ verbose_name_plural = _('Customers')
+ db_table = 'customer'
+
+
+class ServiceProvider(AbstractBaseUser):
+ username_validator = UnicodeUsernameValidator()
+ username = models.CharField(
+ _('username'),
+ max_length=150,
+ unique=True,
+ help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
+ validators=[username_validator],
+ error_messages={
+ 'unique': _("A user with that username already exists."),
+ },
+ )
+ email = models.EmailField(_('email address'), blank=True, unique=True)
+ phone_number = models.CharField(max_length=12, verbose_name=_('phone number'), unique=True)
+ is_staff = models.BooleanField(
+ _('staff status'),
+ default=False,
+ help_text=_('Designates whether the user can log into this admin site.'),
+ )
+ is_active = models.BooleanField(
+ _('active'),
+ default=True,
+ help_text=_(
+ 'Designates whether this user should be treated as active. '
+ 'Unselect this instead of deleting accounts.'
+ ),
+ )
+ date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
+
+ objects = UserManager()
+
+ EMAIL_FIELD = 'email'
+ USERNAME_FIELD = 'username'
+ REQUIRED_FIELDS = ['email']
+
+ class Meta:
+ verbose_name = _('Service provider')
+ verbose_name_plural = _('Service providers')
+ db_table = 'service_provider'
diff --git a/accounts/templates/accounts/base.html b/accounts/templates/accounts/base.html
new file mode 100644
index 0000000..8a93e4f
--- /dev/null
+++ b/accounts/templates/accounts/base.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+ {% block title %} {% endblock %}
+
+
+
+ {% if messages %}
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+ {% endif %}
+ {% block content %}
+ {% endblock %}
+
+
+
+
\ No newline at end of file
diff --git a/accounts/templates/accounts/customer/change_password.html b/accounts/templates/accounts/customer/change_password.html
new file mode 100644
index 0000000..5c71871
--- /dev/null
+++ b/accounts/templates/accounts/customer/change_password.html
@@ -0,0 +1,22 @@
+{% extends 'accounts/base.html' %}
+{% block title %} Change Password {% endblock %}
+{% block content %}
+ Change your password
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/customer/login_register.html b/accounts/templates/accounts/customer/login_register.html
new file mode 100644
index 0000000..d31e4da
--- /dev/null
+++ b/accounts/templates/accounts/customer/login_register.html
@@ -0,0 +1,22 @@
+{% extends 'accounts/base.html' %}
+{% block title %} Login - Register {% endblock %}
+{% block content %}
+ Register - Login
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/customer/password_confirm.html b/accounts/templates/accounts/customer/password_confirm.html
new file mode 100644
index 0000000..b37682c
--- /dev/null
+++ b/accounts/templates/accounts/customer/password_confirm.html
@@ -0,0 +1,22 @@
+{% extends 'accounts/base.html' %}
+{% block title %} Password Confirm {% endblock %}
+{% block content %}
+ Confirm your password
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/customer/password_set.html b/accounts/templates/accounts/customer/password_set.html
new file mode 100644
index 0000000..7c49adb
--- /dev/null
+++ b/accounts/templates/accounts/customer/password_set.html
@@ -0,0 +1,22 @@
+{% extends 'accounts/base.html' %}
+{% block title %} Password Set {% endblock %}
+{% block content %}
+ Set Password
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/customer/phone_number_confirm.html b/accounts/templates/accounts/customer/phone_number_confirm.html
new file mode 100644
index 0000000..675a140
--- /dev/null
+++ b/accounts/templates/accounts/customer/phone_number_confirm.html
@@ -0,0 +1,22 @@
+{% extends 'accounts/base.html' %}
+{% block title %} Phone number confirm {% endblock %}
+{% block content %}
+ Confirm your phone number
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/customer/profile.html b/accounts/templates/accounts/customer/profile.html
new file mode 100644
index 0000000..5570ffd
--- /dev/null
+++ b/accounts/templates/accounts/customer/profile.html
@@ -0,0 +1,23 @@
+{% extends 'base.html' %}
+{% block title %} Profile {% endblock %}
+{% block content %}
+ Profile
+ Phone number : {{ request.user.phone_number }}
+ First name : {{ request.user.first_name }}
+ Last name : {{ request.user.last_name }}
+
+ Address list
+ Orders
+ Create new address
+
+ Update profile
+
+ {% if not request.user.password %}
+ Set password
+
+ {% else %}
+ Change password
+
+ {% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/customer/profile_update.html b/accounts/templates/accounts/customer/profile_update.html
new file mode 100644
index 0000000..706ccd2
--- /dev/null
+++ b/accounts/templates/accounts/customer/profile_update.html
@@ -0,0 +1,22 @@
+{% extends 'accounts/base.html' %}
+{% block title %} Profile Update {% endblock %}
+{% block content %}
+ Update your profile
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/service_provider/change_password.html b/accounts/templates/accounts/service_provider/change_password.html
new file mode 100644
index 0000000..2589839
--- /dev/null
+++ b/accounts/templates/accounts/service_provider/change_password.html
@@ -0,0 +1,9 @@
+{% extends 'accounts/base.html' %}
+{% block content %}
+ Change your password
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/service_provider/login.html b/accounts/templates/accounts/service_provider/login.html
new file mode 100644
index 0000000..3a9ada0
--- /dev/null
+++ b/accounts/templates/accounts/service_provider/login.html
@@ -0,0 +1,8 @@
+{% extends 'accounts/base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/accounts/templates/accounts/service_provider/profile.html b/accounts/templates/accounts/service_provider/profile.html
new file mode 100644
index 0000000..69b086f
--- /dev/null
+++ b/accounts/templates/accounts/service_provider/profile.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% block content %}
+ Profile
+ Your username : {{ request.user.username }}
+ Your email : {{ request.user.email }}
+ Your phone number : {{ request.user.phone_number }}
+
+ Service List
+
+ Change password
+{% endblock %}
diff --git a/accounts/templates/accounts/service_provider/registration.html b/accounts/templates/accounts/service_provider/registration.html
new file mode 100644
index 0000000..3a9ada0
--- /dev/null
+++ b/accounts/templates/accounts/service_provider/registration.html
@@ -0,0 +1,8 @@
+{% extends 'accounts/base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/accounts/templatetags/__init__.py b/accounts/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/accounts/templatetags/accounts_tags.py b/accounts/templatetags/accounts_tags.py
new file mode 100644
index 0000000..ea17538
--- /dev/null
+++ b/accounts/templatetags/accounts_tags.py
@@ -0,0 +1,29 @@
+from django import template
+from django.contrib.auth import get_user_model
+
+from accounts.models import Customer, ServiceProvider
+
+User = get_user_model()
+
+register = template.Library()
+
+
+@register.simple_tag
+def is_customer(user):
+ if isinstance(user, Customer):
+ return True
+ return False
+
+
+@register.simple_tag
+def is_service_provider(user):
+ if isinstance(user, ServiceProvider):
+ return True
+ return False
+
+
+@register.simple_tag
+def is_admin_user(user):
+ if isinstance(user, User):
+ return True
+ return False
diff --git a/accounts/tests.py b/accounts/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/accounts/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/accounts/urls.py b/accounts/urls.py
new file mode 100644
index 0000000..8a65417
--- /dev/null
+++ b/accounts/urls.py
@@ -0,0 +1,29 @@
+from django.urls import path, reverse_lazy
+from django.contrib.auth.views import LogoutView
+
+from accounts.views import CustomerLoginRegisterView, CustomerPhoneNumberConfirmView, CustomerPasswordConfirmView, \
+ CustomerProfileView, CustomerSetPasswordView, ServiceProviderRegistrationView, ServiceProviderLoginView, \
+ ServiceProviderProfileView, CustomerProfileUpdateView, CustomerChangePasswordView, \
+ ServiceProviderChangePasswordView, CustomerLogoutView
+
+app_name = 'accounts'
+
+urlpatterns = [
+ path('customer/login-register/', CustomerLoginRegisterView.as_view(), name='customer-login-register'),
+ path('customer/code-confirm/', CustomerPhoneNumberConfirmView.as_view(), name='customer-code-confirm'),
+ path('customer/password-confirm/', CustomerPasswordConfirmView.as_view(), name='customer-password-confirm'),
+ path('customer/profile/', CustomerProfileView.as_view(), name='customer-profile'),
+ path('customer/profile-update/', CustomerProfileUpdateView.as_view(), name='customer-profile-update'),
+ path('customer/set-password/', CustomerSetPasswordView.as_view(), name='customer-set-password'),
+ path('customer/change-password/', CustomerChangePasswordView.as_view(), name='customer-change-password'),
+ path('customer/logout/', CustomerLogoutView.as_view(), name='customer-logout'),
+ path(
+ 'serviceprovider/registration/', ServiceProviderRegistrationView.as_view(), name='service-provider-registration'
+ ),
+ path('serviceprovider/login/', ServiceProviderLoginView.as_view(), name='service-provider-login'),
+ path('serviceprovider/profile/', ServiceProviderProfileView.as_view(), name='service-provider-profile'),
+ path('serviceprovider/change-password/', ServiceProviderChangePasswordView.as_view(),
+ name='service-provider-change-password'),
+ path('serviceprovider/logout/', LogoutView.as_view(next_page=reverse_lazy('accounts:service-provider-login')),
+ name='service-provider-logout'),
+]
diff --git a/accounts/utils.py b/accounts/utils.py
new file mode 100644
index 0000000..ff757c0
--- /dev/null
+++ b/accounts/utils.py
@@ -0,0 +1,55 @@
+from datetime import datetime, timedelta
+from random import randint
+
+from django.contrib.auth.mixins import UserPassesTestMixin
+from django.core.exceptions import ValidationError
+
+from accounts.models import Customer, ServiceProvider
+
+
+def phone_number_validator(value):
+ if not value.startswith('98') or len(value) != 12:
+ raise ValidationError('phone number must be like 98912*******')
+
+
+def check_expire_time(request):
+ try:
+ expire_time = datetime.strptime(request.session['created-time'], '%Y-%m-%d %H:%M:%S')
+ except KeyError:
+ expire_time = None
+
+ if expire_time:
+ now = datetime.strptime(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')
+ if (now - expire_time) > timedelta(minutes=2):
+ del request.session['code']
+ del request.session['created-time']
+
+
+def set_phone_number_session(request, phone_number):
+ request.session['phone_number'] = phone_number
+ request.session['code'] = randint(1000, 9999)
+ request.session['created_time'] = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
+ print(request.session['code'])
+ print(request.session['created_time'])
+
+
+def check_is_not_authenticated(user):
+ return not user.is_authenticated
+
+
+def can_set_password(user):
+ return not user.password
+
+
+class IsCustomer(UserPassesTestMixin):
+ raise_exception = True
+
+ def test_func(self):
+ return isinstance(self.request.user, Customer)
+
+
+class IsServiceProvider(UserPassesTestMixin):
+ raise_exception = True
+
+ def test_func(self):
+ return isinstance(self.request.user, ServiceProvider)
diff --git a/accounts/views.py b/accounts/views.py
new file mode 100644
index 0000000..7657085
--- /dev/null
+++ b/accounts/views.py
@@ -0,0 +1,214 @@
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required, user_passes_test as user_test
+from django.contrib.auth.views import PasswordChangeView, LogoutView
+from django.http import HttpResponseRedirect
+from django.shortcuts import redirect
+from django.urls import reverse_lazy
+from django.utils.decorators import method_decorator
+from django.views.decorators.http import require_http_methods
+from django.views.generic import FormView, TemplateView, UpdateView
+from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth.hashers import make_password
+
+from accounts.forms import CustomerLoginRegisterForm, CustomerCodeConfirmForm, CustomerPasswordForm, \
+ CustomerPasswordSetForm, ServiceProviderRegistrationForm, ServiceProviderLoginForm, CustomerProfileUpdateForm
+from accounts.models import Customer
+from accounts.utils import check_expire_time, set_phone_number_session, check_is_not_authenticated, \
+ IsCustomer, IsServiceProvider
+from library.utils import CustomUserPasses
+
+
+@method_decorator(require_http_methods(['GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerProfileView(IsCustomer, TemplateView):
+ template_name = 'accounts/customer/profile.html'
+
+
+@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch')
+@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:customer-profile')),
+ name='dispatch')
+class CustomerLoginRegisterView(FormView):
+ form_class = CustomerLoginRegisterForm
+ template_name = 'accounts/customer/login_register.html'
+ success_url = reverse_lazy('accounts:customer-code-confirm')
+
+ def form_valid(self, form):
+ phone_number = form.cleaned_data['phone_number']
+ try:
+ customer = Customer.objects.get(phone_number=phone_number)
+ except Customer.DoesNotExist:
+ set_phone_number_session(self.request, phone_number)
+ else:
+ if customer.password:
+ self.success_url = reverse_lazy('accounts:customer-password-confirm')
+ self.request.session['phone_number'] = phone_number
+ else:
+ set_phone_number_session(self.request, phone_number)
+
+ return super().form_valid(form)
+
+
+@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch')
+@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:customer-profile')),
+ name='dispatch')
+class CustomerPhoneNumberConfirmView(FormView):
+ form_class = CustomerCodeConfirmForm
+ template_name = 'accounts/customer/phone_number_confirm.html'
+ success_url = reverse_lazy('accounts:customer-profile')
+
+ def dispatch(self, request, *args, **kwargs):
+ check_expire_time(request)
+ return super().dispatch(request, *args, **kwargs)
+
+ def form_valid(self, form):
+ phone_number = self.request.session['phone_number']
+ form_code = int(form.cleaned_data['code'])
+ session_code = self.request.session.get('code', None)
+
+ if session_code:
+ if session_code == form_code:
+ Customer.objects.get_or_create(phone_number=phone_number)
+ self.delete_confirm_code()
+ customer = authenticate(phone_number=phone_number)
+ if customer:
+ login(self.request, customer)
+ messages.info(self.request, 'Login success', 'success')
+ return super().form_valid(form)
+ else:
+ return super().form_valid(form)
+
+ else:
+ messages.info(self.request, 'The code is incorrect!', 'danger')
+ return redirect('accounts:customer-code-confirm')
+ else:
+ messages.info(self.request, 'The code is invalid! Enter your phone number again', 'danger')
+ return redirect('accounts:customer-login-register')
+
+ def delete_confirm_code(self):
+ del self.request.session['code']
+
+
+@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch')
+@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:customer-profile')),
+ name='dispatch')
+class CustomerPasswordConfirmView(FormView):
+ form_class = CustomerPasswordForm
+ template_name = 'accounts/customer/password_confirm.html'
+ success_url = reverse_lazy('accounts:customer-profile')
+
+ def form_valid(self, form):
+ phone_number = self.request.session['phone_number']
+ password = form.cleaned_data['password']
+ customer = authenticate(phone_number=phone_number, password=password)
+
+ if customer:
+ login(self.request, customer)
+ messages.info(self.request, 'Login success', 'success')
+ return super().form_valid(form)
+
+ else:
+ messages.info(self.request, 'Your password is incorrect!', 'danger')
+ return redirect('accounts:customer-password-confirm')
+
+
+@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerSetPasswordView(CustomUserPasses, UpdateView):
+ model = Customer
+ form_class = CustomerPasswordSetForm
+ success_url = reverse_lazy('accounts:customer-login-register')
+ template_name = 'accounts/customer/password_set.html'
+
+ def test_func(self):
+ if not isinstance(self.request.user, Customer):
+ return False
+ if self.request.user.password:
+ return False, True, reverse_lazy("accounts:customer-change-password")
+
+ return True
+
+ def get_object(self, queryset=None):
+ return self.request.user
+
+
+@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerProfileUpdateView(IsCustomer, UpdateView):
+ model = Customer
+ form_class = CustomerProfileUpdateForm
+ success_url = reverse_lazy('accounts:customer-profile')
+ template_name = 'accounts/customer/profile_update.html'
+
+ def get_object(self, queryset=None):
+ return self.request.user
+
+
+@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerChangePasswordView(CustomUserPasses, PasswordChangeView):
+ template_name = 'accounts/customer/change_password.html'
+ success_url = reverse_lazy('accounts:customer-profile')
+
+ def test_func(self):
+ if not isinstance(self.request.user, Customer):
+ return False
+ if not self.request.user.password:
+ return False, True, reverse_lazy("accounts:customer-set-password")
+ return True
+
+
+class CustomerLogoutView(LogoutView):
+ next_page = reverse_lazy('accounts:customer-login-register')
+
+ def dispatch(self, request, *args, **kwargs):
+ logout(request)
+ next_page = self.get_next_page()
+ response = HttpResponseRedirect(next_page)
+ if request.COOKIES.get('cart_id', None) is not None:
+ response.delete_cookie('cart_id')
+ return response
+
+
+@method_decorator(require_http_methods(['POST', 'GET']), name='dispatch')
+@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:service-provider-profile')),
+ name='dispatch')
+class ServiceProviderRegistrationView(FormView):
+ form_class = ServiceProviderRegistrationForm
+ template_name = 'accounts/service_provider/registration.html'
+ success_url = reverse_lazy('accounts:service-provider-login')
+
+ def form_valid(self, form):
+ instance = form.save(commit=False)
+ instance.password = make_password(instance.password)
+ instance.save()
+ return super().form_valid(form)
+
+
+@method_decorator(require_http_methods(['POST', 'GET']), name='dispatch')
+@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:service-provider-profile')),
+ name='dispatch')
+class ServiceProviderLoginView(FormView):
+ form_class = ServiceProviderLoginForm
+ template_name = 'accounts/service_provider/login.html'
+ success_url = reverse_lazy('accounts:service-provider-profile')
+
+ def form_valid(self, form):
+ user = form.cleaned_data['user']
+ user_authenticated = authenticate(username=user.username, password=user.password)
+ if user_authenticated:
+ login(self.request, user_authenticated)
+ return super().form_valid(form)
+
+
+@method_decorator(require_http_methods(['GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderProfileView(IsServiceProvider, TemplateView):
+ template_name = 'accounts/service_provider/profile.html'
+ raise_exception = True
+
+
+@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class ServiceProviderChangePasswordView(IsServiceProvider, PasswordChangeView):
+ template_name = 'accounts/service_provider/change_password.html'
+ success_url = reverse_lazy('accounts:service-provider-profile')
diff --git a/address/__init__.py b/address/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/address/admin.py b/address/admin.py
new file mode 100644
index 0000000..4934784
--- /dev/null
+++ b/address/admin.py
@@ -0,0 +1,36 @@
+from django.contrib import admin
+from .models import State, City, Area, ServiceAddress, CustomerAddress
+
+
+@admin.register(State)
+class StateAdmin(admin.ModelAdmin):
+ list_display = ('name', 'slug')
+ search_fields = ('name',)
+
+
+@admin.register(City)
+class CityAdmin(admin.ModelAdmin):
+ list_display = ('name', 'slug', 'state')
+ list_filter = ('state',)
+ search_fields = ('name',)
+
+
+@admin.register(Area)
+class AreaAdmin(admin.ModelAdmin):
+ list_display = ('name', 'slug', 'city')
+ list_filter = ('city',)
+ search_fields = ('name',)
+
+
+@admin.register(ServiceAddress)
+class ServiceAddressAdmin(admin.ModelAdmin):
+ list_display = ('state', 'city', 'area', 'floor', 'plaque')
+ list_filter = ('state', 'city')
+ search_fields = ('area', 'floor', 'plaque')
+
+
+@admin.register(CustomerAddress)
+class CustomerAddressAdmin(admin.ModelAdmin):
+ list_display = ('customer_user', 'state', 'city', 'area', 'floor', 'plaque')
+ list_filter = ('state', 'city')
+ search_fields = ('area', 'floor', 'plaque')
diff --git a/address/apps.py b/address/apps.py
new file mode 100644
index 0000000..572d7e0
--- /dev/null
+++ b/address/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AddressConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'address'
diff --git a/address/forms.py b/address/forms.py
new file mode 100644
index 0000000..5d9d061
--- /dev/null
+++ b/address/forms.py
@@ -0,0 +1,28 @@
+from django import forms
+
+from address.models import CustomerAddress, ServiceAddress
+
+base_fields = ('state', 'city', 'area', 'street', 'alley', 'floor', 'plaque')
+base_widgets = {
+ 'state': forms.Select(attrs={'class': 'form-control'}),
+ 'city': forms.Select(attrs={'class': 'form-control'}),
+ 'area': forms.Select(attrs={'class': 'form-control'}),
+ 'street': forms.TextInput(attrs={'class': 'form-control'}),
+ 'alley': forms.TextInput(attrs={'class': 'form-control'}),
+ 'floor': forms.NumberInput(attrs={'class': 'form-control'}),
+ 'plaque': forms.NumberInput(attrs={'class': 'form-control'}),
+}
+
+
+class CustomerAddressCreateUpdateForm(forms.ModelForm):
+ class Meta:
+ model = CustomerAddress
+ fields = base_fields
+ widgets = base_widgets
+
+
+class ServiceAddressCreateUpdateForm(forms.ModelForm):
+ class Meta:
+ model = ServiceAddress
+ fields = base_fields
+ widgets = base_widgets
diff --git a/address/migrations/0001_initial.py b/address/migrations/0001_initial.py
new file mode 100644
index 0000000..57ac86c
--- /dev/null
+++ b/address/migrations/0001_initial.py
@@ -0,0 +1,80 @@
+# Generated by Django 3.2 on 2021-08-28 20:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='State',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('name', models.CharField(max_length=20, verbose_name='name')),
+ ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')),
+ ],
+ options={
+ 'verbose_name': 'State',
+ 'verbose_name_plural': 'States',
+ 'db_table': 'state',
+ },
+ ),
+ migrations.CreateModel(
+ name='City',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('name', models.CharField(max_length=20, verbose_name='name')),
+ ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')),
+ ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cities', to='address.state', verbose_name='state')),
+ ],
+ options={
+ 'verbose_name': 'City',
+ 'verbose_name_plural': 'Cities',
+ 'db_table': 'city',
+ },
+ ),
+ migrations.CreateModel(
+ name='Area',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('name', models.CharField(max_length=20, verbose_name='name')),
+ ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')),
+ ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='areas', to='address.city', verbose_name='city')),
+ ],
+ options={
+ 'verbose_name': 'Area',
+ 'verbose_name_plural': 'Areas',
+ 'db_table': 'area',
+ },
+ ),
+ migrations.CreateModel(
+ name='Address',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('floor', models.SmallIntegerField(verbose_name='floor')),
+ ('plaque', models.SmallIntegerField(verbose_name='plaque')),
+ ('area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.area', verbose_name='area')),
+ ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.city', verbose_name='city')),
+ ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.state', verbose_name='state')),
+ ],
+ options={
+ 'verbose_name': 'Address',
+ 'verbose_name_plural': 'Addresses',
+ 'db_table': 'address',
+ },
+ ),
+ ]
diff --git a/address/migrations/0002_auto_20210830_1305.py b/address/migrations/0002_auto_20210830_1305.py
new file mode 100644
index 0000000..7350549
--- /dev/null
+++ b/address/migrations/0002_auto_20210830_1305.py
@@ -0,0 +1,55 @@
+# Generated by Django 3.2 on 2021-08-30 08:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CustomerAddress',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('floor', models.SmallIntegerField(verbose_name='floor')),
+ ('plaque', models.SmallIntegerField(verbose_name='plaque')),
+ ],
+ options={
+ 'verbose_name': 'CustomerAddress',
+ 'verbose_name_plural': 'CustomerAddresses',
+ 'db_table': 'customer_address',
+ },
+ ),
+ migrations.CreateModel(
+ name='ServiceAddress',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('floor', models.SmallIntegerField(verbose_name='floor')),
+ ('plaque', models.SmallIntegerField(verbose_name='plaque')),
+ ],
+ options={
+ 'verbose_name': 'ServiceAddress',
+ 'verbose_name_plural': 'ServiceAddresses',
+ 'db_table': 'service_address',
+ },
+ ),
+ migrations.RemoveField(
+ model_name='address',
+ name='area',
+ ),
+ migrations.RemoveField(
+ model_name='address',
+ name='city',
+ ),
+ migrations.RemoveField(
+ model_name='address',
+ name='state',
+ ),
+ ]
diff --git a/address/migrations/0003_auto_20210830_1305.py b/address/migrations/0003_auto_20210830_1305.py
new file mode 100644
index 0000000..b2a0ccc
--- /dev/null
+++ b/address/migrations/0003_auto_20210830_1305.py
@@ -0,0 +1,55 @@
+# Generated by Django 3.2 on 2021-08-30 08:35
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('service', '0002_alter_service_address'),
+ ('address', '0002_auto_20210830_1305'),
+ ('accounts', '0001_initial'),
+ ('payment', '0002_alter_invoice_address'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='Address',
+ ),
+ migrations.AddField(
+ model_name='serviceaddress',
+ name='area',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.area', verbose_name='area'),
+ ),
+ migrations.AddField(
+ model_name='serviceaddress',
+ name='city',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.city', verbose_name='city'),
+ ),
+ migrations.AddField(
+ model_name='serviceaddress',
+ name='state',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.state', verbose_name='state'),
+ ),
+ migrations.AddField(
+ model_name='customeraddress',
+ name='area',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.area', verbose_name='area'),
+ ),
+ migrations.AddField(
+ model_name='customeraddress',
+ name='city',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.city', verbose_name='city'),
+ ),
+ migrations.AddField(
+ model_name='customeraddress',
+ name='state',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.state', verbose_name='state'),
+ ),
+ migrations.AddField(
+ model_name='customeraddress',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='accounts.customer', verbose_name='customer'),
+ ),
+ ]
diff --git a/address/migrations/0004_auto_20210830_1327.py b/address/migrations/0004_auto_20210830_1327.py
new file mode 100644
index 0000000..6577812
--- /dev/null
+++ b/address/migrations/0004_auto_20210830_1327.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.2 on 2021-08-30 08:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0003_auto_20210830_1305'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='customeraddress',
+ name='alley',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='customeraddress',
+ name='street',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='serviceaddress',
+ name='alley',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='serviceaddress',
+ name='street',
+ field=models.TextField(blank=True),
+ ),
+ ]
diff --git a/address/migrations/0005_auto_20210830_1329.py b/address/migrations/0005_auto_20210830_1329.py
new file mode 100644
index 0000000..dc9e112
--- /dev/null
+++ b/address/migrations/0005_auto_20210830_1329.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.2 on 2021-08-30 08:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0004_auto_20210830_1327'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='customeraddress',
+ name='alley',
+ field=models.CharField(max_length=30),
+ ),
+ migrations.AlterField(
+ model_name='customeraddress',
+ name='street',
+ field=models.CharField(max_length=50),
+ ),
+ migrations.AlterField(
+ model_name='serviceaddress',
+ name='alley',
+ field=models.CharField(max_length=30),
+ ),
+ migrations.AlterField(
+ model_name='serviceaddress',
+ name='street',
+ field=models.CharField(max_length=50),
+ ),
+ ]
diff --git a/address/migrations/0006_rename_user_customeraddress_customer_user.py b/address/migrations/0006_rename_user_customeraddress_customer_user.py
new file mode 100644
index 0000000..3b35923
--- /dev/null
+++ b/address/migrations/0006_rename_user_customeraddress_customer_user.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2 on 2021-08-31 10:06
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0005_auto_20210830_1329'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='customeraddress',
+ old_name='user',
+ new_name='customer_user',
+ ),
+ ]
diff --git a/address/migrations/__init__.py b/address/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/address/models.py b/address/models.py
new file mode 100644
index 0000000..bedac10
--- /dev/null
+++ b/address/models.py
@@ -0,0 +1,90 @@
+from django.db import models
+
+from accounts.models import Customer
+from library.models import BaseModel
+from django.utils.translation import ugettext_lazy as _
+
+
+class State(BaseModel):
+ name = models.CharField(max_length=20, verbose_name=_('name'))
+ slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ verbose_name = _('State')
+ verbose_name_plural = _('States')
+ db_table = 'state'
+
+
+class City(BaseModel):
+ name = models.CharField(max_length=20, verbose_name=_('name'))
+ slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True)
+ state = models.ForeignKey(State, verbose_name=_('state'), related_name='cities', on_delete=models.CASCADE)
+
+ def __str__(self):
+ return f'{self.state} - {self.name}'
+
+ class Meta:
+ verbose_name = _('City')
+ verbose_name_plural = _('Cities')
+ db_table = 'city'
+
+
+class Area(BaseModel):
+ name = models.CharField(max_length=20, verbose_name=_('name'))
+ slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True)
+ city = models.ForeignKey(City, verbose_name=_('city'), related_name='areas', on_delete=models.CASCADE)
+
+ def __str__(self):
+ return f'{self.city.name} - {self.name}'
+
+ class Meta:
+ verbose_name = _('Area')
+ verbose_name_plural = _('Areas')
+ db_table = 'area'
+
+
+class BaseAddress(BaseModel):
+ street = models.CharField(max_length=50)
+ alley = models.CharField(max_length=30)
+ floor = models.SmallIntegerField(verbose_name=_('floor'))
+ plaque = models.SmallIntegerField(verbose_name=_('plaque'))
+
+ class Meta:
+ abstract = True
+
+
+class CustomerAddress(BaseAddress):
+ customer_user = models.ForeignKey(
+ Customer,
+ verbose_name=_('customer'),
+ related_name='c_addresses',
+ on_delete=models.CASCADE
+ )
+ state = models.ForeignKey(State, verbose_name=_('state'), related_name='c_addresses', on_delete=models.CASCADE)
+ city = models.ForeignKey(City, verbose_name=_('city'), related_name='c_addresses', on_delete=models.CASCADE)
+ area = models.ForeignKey(Area, verbose_name=_('area'), related_name='c_addresses', on_delete=models.CASCADE)
+
+ def __str__(self):
+ return f'{self.customer_user} - {self.city} - {self.area}'
+
+ class Meta:
+ verbose_name = _('CustomerAddress')
+ verbose_name_plural = _('CustomerAddresses')
+ db_table = 'customer_address'
+
+
+class ServiceAddress(BaseAddress):
+ state = models.ForeignKey(State, verbose_name=_('state'), related_name='s_addresses', on_delete=models.CASCADE)
+ city = models.ForeignKey(City, verbose_name=_('city'), related_name='s_addresses', on_delete=models.CASCADE)
+ area = models.ForeignKey(Area, verbose_name=_('area'), related_name='s_addresses', on_delete=models.CASCADE)
+
+ def __str__(self):
+ return f'{self.area.name}، {self.street}، {self.alley}'
+
+ class Meta:
+ verbose_name = _('ServiceAddress')
+ verbose_name_plural = _('ServiceAddresses')
+ db_table = 'service_address'
diff --git a/address/templates/address/create_update_form.html b/address/templates/address/create_update_form.html
new file mode 100644
index 0000000..a7b816f
--- /dev/null
+++ b/address/templates/address/create_update_form.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% load accounts_tags %}
+{% block content %}
+
+ {% is_service_provider request.user as is_provider %}
+ {% is_customer request.user as is_customer %}
+ {% if is_provider %}
+ {% if service %}
+ cancel
+
+ {% else %}
+ cancel
+ {% endif %}
+ {% elif is_customer %}
+ cancel
+ {% endif %}
+{% endblock %}
diff --git a/address/templates/address/customer_address_list.html b/address/templates/address/customer_address_list.html
new file mode 100644
index 0000000..a3e7908
--- /dev/null
+++ b/address/templates/address/customer_address_list.html
@@ -0,0 +1,19 @@
+{% extends 'base.html' %}
+{% block content %}
+ Create new address
+
+
+ {% for address in addresses %}
+
+ {{ address.state.name }} - {{ address.city.name }} - {{ address.area.name }} - {{ address.street }}
+ - {{ address.alley }} - {{ address.floor }} - {{ address.plaque }}
+
+
+ {% endfor %}
+
+{% endblock %}
diff --git a/address/templates/address/delete_form.html b/address/templates/address/delete_form.html
new file mode 100644
index 0000000..452edf9
--- /dev/null
+++ b/address/templates/address/delete_form.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/address/tests.py b/address/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/address/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/address/urls.py b/address/urls.py
new file mode 100644
index 0000000..c85a8c8
--- /dev/null
+++ b/address/urls.py
@@ -0,0 +1,17 @@
+from django.urls import path
+
+from .views import CustomerAddressCreateView, CustomerAddressUpdateView, CustomerAddressDeleteView, \
+ ServiceAddressCreateView, ServiceAddressUpdateView, CustomerAddressListView
+
+app_name = 'address'
+
+urlpatterns = [
+ path('customer/create/', CustomerAddressCreateView.as_view(), name='customer-address-create'),
+ path('service/create//', ServiceAddressCreateView.as_view(), name='service-address-create'),
+
+ path('customer/update//', CustomerAddressUpdateView.as_view(), name='customer-address-update'),
+ path('service/update//', ServiceAddressUpdateView.as_view(), name='service-address-update'),
+
+ path('customer/delete//', CustomerAddressDeleteView.as_view(), name='customer-address-delete'),
+ path('customer/list/', CustomerAddressListView.as_view(), name='customer-address-list'),
+]
diff --git a/address/views.py b/address/views.py
new file mode 100644
index 0000000..b041e85
--- /dev/null
+++ b/address/views.py
@@ -0,0 +1,115 @@
+from django.contrib.auth.decorators import login_required
+from django.db import transaction
+from django.shortcuts import get_object_or_404
+from django.urls import reverse_lazy
+from django.utils.decorators import method_decorator
+from django.views.generic import CreateView, UpdateView, DeleteView, ListView
+
+from accounts.models import ServiceProvider
+from accounts.utils import IsCustomer, IsServiceProvider
+from address.forms import CustomerAddressCreateUpdateForm, ServiceAddressCreateUpdateForm
+from address.models import CustomerAddress, ServiceAddress
+from library.utils import CustomUserPasses
+from service.models import Service
+
+
+class BaseAddress:
+ model = CustomerAddress
+ form_class = CustomerAddressCreateUpdateForm
+ template_name = 'address/create_update_form.html'
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerAddressCreateView(BaseAddress, IsCustomer, CreateView):
+ success_url = reverse_lazy('accounts:customer-profile')
+
+ def form_valid(self, form):
+ instance = form.save(commit=False)
+ instance.customer_user = self.request.user
+ instance.save()
+ return super().form_valid(form)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerAddressUpdateView(BaseAddress, IsCustomer, UpdateView):
+ success_url = reverse_lazy('address:customer-address-list')
+
+ def test_func(self):
+ result = super().test_func()
+ obj = self.get_object()
+ return result and obj.customer_user == self.request.user
+
+ def form_valid(self, form):
+ instance = form.save(commit=False)
+ instance.user = self.request.user
+ instance.save()
+ return super().form_valid(form)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerAddressDeleteView(IsCustomer, DeleteView):
+ model = CustomerAddress
+ template_name = 'address/delete_form.html'
+ success_url = reverse_lazy('accounts:customer-profile')
+
+ def test_func(self):
+ result = super().test_func()
+ obj = self.get_object()
+ return result and obj.customer_user == self.request.user
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerAddressListView(ListView):
+ model = CustomerAddress
+ template_name = 'address/customer_address_list.html'
+ context_object_name = 'addresses'
+
+ def get_queryset(self):
+ return super().get_queryset().filter(customer_user=self.request.user)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceAddressCreateView(CustomUserPasses, CreateView):
+ model = ServiceAddress
+ form_class = ServiceAddressCreateUpdateForm
+ template_name = 'address/create_update_form.html'
+ success_url = reverse_lazy('accounts:service-provider-profile')
+ raise_exception = True
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.service = get_object_or_404(Service, id=self.kwargs['service_pk'])
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['service'] = self.service
+ return context
+
+ def test_func(self):
+ if not isinstance(self.request.user, ServiceProvider):
+ return False
+ if self.service.service_provider != self.request.user:
+ return False
+ if self.service.address:
+ return False, True, reverse_lazy('accounts:service-provider-profile')
+ return True
+
+ def form_valid(self, form):
+ with transaction.atomic():
+ instance = form.save()
+ self.service.address = instance
+ self.service.save()
+ return super().form_valid(form)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceAddressUpdateView(IsServiceProvider, UpdateView):
+ model = ServiceAddress
+ form_class = ServiceAddressCreateUpdateForm
+ template_name = 'address/create_update_form.html'
+ success_url = reverse_lazy('accounts:service-provider-profile')
+
+ def test_func(self):
+ result = super().test_func()
+ obj = self.get_object()
+ return result and obj.services.service_provider == self.request.user
diff --git a/cart/__init__.py b/cart/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/cart/admin.py b/cart/admin.py
new file mode 100644
index 0000000..9db535e
--- /dev/null
+++ b/cart/admin.py
@@ -0,0 +1,16 @@
+from django.contrib import admin
+
+from cart.models import Cart, CartLine
+
+
+class CartLineInline(admin.TabularInline):
+ model = CartLine
+ extra = 0
+
+
+@admin.register(Cart)
+class CartAdmin(admin.ModelAdmin):
+ list_display = ('customer', 'is_paid')
+ list_filter = ('is_paid',)
+ search_fields = ('customer__phone_number',)
+ inlines = [CartLineInline]
diff --git a/cart/apps.py b/cart/apps.py
new file mode 100644
index 0000000..f3e3ec9
--- /dev/null
+++ b/cart/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class CartConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'cart'
diff --git a/cart/migrations/0001_initial.py b/cart/migrations/0001_initial.py
new file mode 100644
index 0000000..790aa33
--- /dev/null
+++ b/cart/migrations/0001_initial.py
@@ -0,0 +1,54 @@
+# Generated by Django 3.2 on 2021-09-07 12:46
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('payment', '0002_alter_invoice_address'),
+ ('item', '0002_alter_item_upc'),
+ ('accounts', '0002_auto_20210831_0046'),
+ ('service', '0008_service_available'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Cart',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('is_paid', models.BooleanField(default=False, verbose_name='is paid')),
+ ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='carts', to='accounts.customer', verbose_name='customer')),
+ ('invoice', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cart', to='payment.invoice', verbose_name='invoice')),
+ ('service', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='carts', to='service.service', verbose_name='service')),
+ ],
+ options={
+ 'verbose_name': 'Cart',
+ 'verbose_name_plural': 'Carts',
+ 'db_table': 'cart',
+ },
+ ),
+ migrations.CreateModel(
+ name='CartLine',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('quantity', models.PositiveIntegerField(default=1, verbose_name='quantity')),
+ ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='cart.cart', verbose_name='cart')),
+ ('item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='lines', to='item.item', verbose_name='item')),
+ ],
+ options={
+ 'verbose_name': 'Cart line',
+ 'verbose_name_plural': 'Cart lines',
+ 'db_table': 'cart_line',
+ 'ordering': ('created_time', 'modified_time'),
+ 'unique_together': {('item', 'cart')},
+ },
+ ),
+ ]
\ No newline at end of file
diff --git a/cart/migrations/0002_remove_cart_invoice.py b/cart/migrations/0002_remove_cart_invoice.py
new file mode 100644
index 0000000..7a60ad5
--- /dev/null
+++ b/cart/migrations/0002_remove_cart_invoice.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2 on 2021-09-12 12:27
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cart', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='cart',
+ name='invoice',
+ ),
+ ]
diff --git a/cart/migrations/__init__.py b/cart/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/cart/models.py b/cart/models.py
new file mode 100644
index 0000000..b581a72
--- /dev/null
+++ b/cart/models.py
@@ -0,0 +1,82 @@
+from django.db import models, transaction
+from django.db.models import Sum, F
+from django.utils.translation import gettext as _
+from accounts.models import Customer
+from item.models import Item
+from library.models import BaseModel
+from service.models import Service
+
+
+class Cart(BaseModel):
+ customer = models.ForeignKey(
+ Customer,
+ verbose_name=_('customer'),
+ null=True, blank=True,
+ related_name='carts',
+ on_delete=models.CASCADE
+ )
+ is_paid = models.BooleanField(default=False, verbose_name=_('is paid'))
+ service = models.ForeignKey(
+ Service,
+ verbose_name=_('service'),
+ related_name='carts',
+ on_delete=models.PROTECT,
+ null=True
+ )
+
+ def __str__(self):
+ return f"{self.customer} - {'Paid' if self.is_paid else 'Not paid'}"
+
+ @property
+ def total_price(self):
+ return self.lines.all().annotate(price=F('quantity') * F('item__price')).aggregate(
+ total_price=Sum('price')).get('total_price')
+
+ @classmethod
+ def get_cart(cls, cart_id):
+ if cart_id is None:
+ cart = cls.objects.create()
+ else:
+ cart, created = cls.objects.get_or_create(pk=cart_id)
+ return cart
+
+ def create_or_increase(self, item):
+ with transaction.atomic():
+
+ if item.service != self.service: # adding the first item or item with different service
+ self.empty_cart()
+ self.service = item.service
+ self.save()
+
+ cart_line, create = self.lines.select_for_update().get_or_create(item=item, defaults={'cart': self})
+ if not create:
+ cart_line.quantity += 1
+ cart_line.save()
+
+ def empty_cart(self):
+ self.lines.all().delete()
+
+ class Meta:
+ verbose_name = _('Cart')
+ verbose_name_plural = _('Carts')
+ db_table = 'cart'
+
+
+class CartLine(BaseModel):
+ item = models.ForeignKey(Item, verbose_name=_('item'), related_name='lines', on_delete=models.PROTECT)
+ cart = models.ForeignKey(Cart, verbose_name=_('cart'), related_name='lines', on_delete=models.CASCADE)
+ quantity = models.PositiveIntegerField(default=1, verbose_name=_('quantity'))
+
+ def __str__(self):
+ return f"{self.item} - {self.quantity}"
+
+ @property
+ def price(self):
+ return self.item.price * self.quantity
+
+ class Meta:
+ verbose_name = _('Cart line')
+ verbose_name_plural = _('Cart lines')
+ db_table = 'cart_line'
+ ordering = ('created_time', 'modified_time')
+ unique_together = ('item', 'cart') # each cart can have one cart line with the same item
diff --git a/cart/templatetags/__init__.py b/cart/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/cart/templatetags/cart_tags.py b/cart/templatetags/cart_tags.py
new file mode 100644
index 0000000..520af02
--- /dev/null
+++ b/cart/templatetags/cart_tags.py
@@ -0,0 +1,25 @@
+from django import template
+from django.db.models import Sum, F
+
+
+from cart.models import Cart
+
+register = template.Library()
+
+
+@register.simple_tag
+def get_cart(request, service):
+ try:
+ cart = Cart.objects.get(id=request.COOKIES.get('cart_id', None), is_paid=False)
+ except Cart.DoesNotExist:
+ return None
+ else:
+ if cart.lines.filter(item__service=service).exists():
+ return cart
+ return None
+
+
+@register.simple_tag
+def calculate_total_price(cart):
+ price = cart.lines.annotate(true_price=(F('quantity') * F('item__price'))).aggregate(Sum('true_price'))
+ return price['true_price__sum']
diff --git a/cart/tests.py b/cart/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/cart/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/cart/urls.py b/cart/urls.py
new file mode 100644
index 0000000..17145e0
--- /dev/null
+++ b/cart/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path
+
+from cart.views import AddToCartView, CartLineDeleteView, CartLineDecreaseView, EmptyCartView
+
+app_name = 'cart'
+
+urlpatterns = [
+ path('add/', AddToCartView.as_view(), name='add-to-cart'),
+ path('cartline/delete//', CartLineDeleteView.as_view(), name='cart-line-delete'),
+ path('cartline/decrease//', CartLineDecreaseView.as_view(), name='cart-line-decrease'),
+ path('cart-empty/', EmptyCartView.as_view(), name='empty-cart'),
+
+]
diff --git a/cart/views.py b/cart/views.py
new file mode 100644
index 0000000..d15f740
--- /dev/null
+++ b/cart/views.py
@@ -0,0 +1,106 @@
+from django.http import HttpResponseRedirect, Http404
+from django.shortcuts import get_object_or_404
+from django.urls import reverse_lazy
+from django.utils.decorators import method_decorator
+from django.views.decorators.http import require_http_methods
+from django.views.generic import View
+
+from accounts.models import Customer
+from cart.models import Cart, CartLine
+from item.models import Item
+
+
+class BaseCartView(View):
+ def check_customer(self, cart):
+ user = self.request.user
+ if user.is_authenticated:
+ if not isinstance(user, Customer):
+ raise Http404
+ if cart.customer is None:
+ cart.customer = user
+ cart.save()
+ return cart
+ else:
+ if user != cart.customer:
+ raise Http404
+ return cart
+ elif cart.customer is not None:
+ return Cart.objects.create()
+ else:
+ return cart
+
+ def get_cart_set_cookie(self, redirect_url=None):
+ response = HttpResponseRedirect(redirect_url)
+ cart = Cart.get_cart(self.request.COOKIES.get('cart_id', None))
+ cart = self.check_customer(cart)
+ response.set_cookie('cart_id', cart.pk, max_age=15 * 60)
+ return cart, response
+
+
+@method_decorator(require_http_methods(('POST',)), name='dispatch')
+class AddToCartView(BaseCartView):
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.kwargs['item'] = get_object_or_404(Item, pk=request.POST['item_id'])
+
+ def get_success_url(self):
+ return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['item'].service.pk})
+
+ def post(self, request, *args, **kwargs):
+ cart, response = self.get_cart_set_cookie(self.get_success_url())
+ cart.create_or_increase(self.kwargs['item'])
+ return response
+
+
+@method_decorator(require_http_methods(('POST',)), name='dispatch')
+class CartLineDeleteView(BaseCartView):
+ model = CartLine
+
+ def get_success_url(self):
+ return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['cart_line'].item.service.pk})
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.kwargs['cart_line'] = get_object_or_404(CartLine, pk=self.kwargs['pk'])
+
+ def post(self, request, *args, **kwargs):
+ cart, response = self.get_cart_set_cookie(self.get_success_url())
+ if self.kwargs['cart_line'].cart != cart:
+ raise Http404
+ self.kwargs['cart_line'].delete()
+ return response
+
+
+@method_decorator(require_http_methods(('POST',)), name='dispatch')
+class CartLineDecreaseView(BaseCartView):
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.kwargs['cart_line'] = get_object_or_404(CartLine, id=self.kwargs['pk'])
+
+ def get_success_url(self):
+ return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['cart_line'].item.service.pk})
+
+ def post(self, request, *args, **kwargs):
+ cart, response = self.get_cart_set_cookie(self.get_success_url())
+ if self.kwargs['cart_line'].cart != cart:
+ raise Http404
+
+ if self.kwargs['cart_line'].quantity >= 2:
+ self.kwargs['cart_line'].quantity -= 1
+ self.kwargs['cart_line'].save()
+
+ return response
+
+
+@method_decorator(require_http_methods(('POST',)), name='dispatch')
+class EmptyCartView(BaseCartView):
+
+ def post(self, request, *args, **kwargs):
+ cart = Cart.get_cart(self.request.COOKIES.get('cart_id', None))
+ cart = self.check_customer(cart)
+ cart.empty_cart()
+ cart.save()
+ response = HttpResponseRedirect(reverse_lazy('item:list', kwargs={'service_pk': cart.service.pk}))
+ return response
diff --git a/gateway/__init__.py b/gateway/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gateway/admin.py b/gateway/admin.py
new file mode 100644
index 0000000..344edbf
--- /dev/null
+++ b/gateway/admin.py
@@ -0,0 +1,11 @@
+from django.contrib import admin
+
+from gateway.models import Gateway
+
+
+@admin.register(Gateway)
+class GatewayAdmin(admin.ModelAdmin):
+ list_display = ('title', 'gateway_code', 'is_enable')
+ list_filter = ('is_enable',)
+ search_fields = ('title',)
+
diff --git a/gateway/apps.py b/gateway/apps.py
new file mode 100644
index 0000000..5e3f248
--- /dev/null
+++ b/gateway/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class GatewayConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'gateway'
diff --git a/gateway/migrations/0001_initial.py b/gateway/migrations/0001_initial.py
new file mode 100644
index 0000000..246f5be
--- /dev/null
+++ b/gateway/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.2 on 2021-09-12 12:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Gateway',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('title', models.CharField(max_length=100, verbose_name='title')),
+ ('gateway_request_url', models.CharField(blank=True, max_length=150, verbose_name='request url')),
+ ('gateway_verify_url', models.CharField(blank=True, max_length=150, verbose_name='verify url')),
+ ('gateway_code', models.CharField(choices=[('zarrinpal', 'Zarrinpal'), ('saman', 'Saman')], max_length=20, verbose_name='gateway code')),
+ ('is_enable', models.BooleanField(default=True, verbose_name='is enable')),
+ ('auth_data', models.TextField(blank=True, verbose_name='auth data')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/gateway/migrations/0002_auto_20210912_2211.py b/gateway/migrations/0002_auto_20210912_2211.py
new file mode 100644
index 0000000..f35215f
--- /dev/null
+++ b/gateway/migrations/0002_auto_20210912_2211.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.2 on 2021-09-12 17:41
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('gateway', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='gateway',
+ options={'verbose_name': 'Gateway', 'verbose_name_plural': 'Gateways'},
+ ),
+ migrations.AlterModelTable(
+ name='gateway',
+ table='gateway',
+ ),
+ ]
diff --git a/gateway/migrations/__init__.py b/gateway/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gateway/models.py b/gateway/models.py
new file mode 100644
index 0000000..45fa09b
--- /dev/null
+++ b/gateway/models.py
@@ -0,0 +1,43 @@
+from django.db import models
+from django.utils.translation import gettext as _
+
+from gateway.utils.zarrinpal import zarrinpal_request_handler, zarrinpal_payment_verify
+from library.models import BaseModel
+
+
+class Gateway(BaseModel):
+ FUNCTION_ZARRINPAL = 'zarrinpal'
+ FUNCTION_SAMAN = 'saman'
+ GATEWAY_FUNCTIONS_CHOICES = (
+ (FUNCTION_ZARRINPAL, _('Zarrinpal')),
+ (FUNCTION_SAMAN, _('Saman'))
+ )
+
+ title = models.CharField(max_length=100, verbose_name=_('title'))
+ gateway_request_url = models.CharField(max_length=150, verbose_name=_('request url'), blank=True)
+ gateway_verify_url = models.CharField(max_length=150, verbose_name=_('verify url'), blank=True)
+ gateway_code = models.CharField(max_length=20, verbose_name=_('gateway code'), choices=GATEWAY_FUNCTIONS_CHOICES)
+ is_enable = models.BooleanField(verbose_name=_('is enable'), default=True)
+ auth_data = models.TextField(verbose_name=_('auth data'), blank=True)
+
+ def __str__(self):
+ return self.title
+
+ def get_request_handler(self):
+ handlers = {
+ self.FUNCTION_ZARRINPAL: zarrinpal_request_handler,
+ self.FUNCTION_SAMAN: None,
+ }
+ return handlers[self.gateway_code]
+
+ def get_verify_handler(self):
+ handlers = {
+ self.FUNCTION_ZARRINPAL: zarrinpal_payment_verify,
+ self.FUNCTION_SAMAN: None,
+ }
+ return handlers[self.gateway_code]
+
+ class Meta:
+ verbose_name = _('Gateway')
+ verbose_name_plural = _('Gateways')
+ db_table = 'gateway'
diff --git a/gateway/tests.py b/gateway/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/gateway/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/gateway/utils/__init__.py b/gateway/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gateway/utils/zarrinpal.py b/gateway/utils/zarrinpal.py
new file mode 100644
index 0000000..fb7d910
--- /dev/null
+++ b/gateway/utils/zarrinpal.py
@@ -0,0 +1,19 @@
+from suds.client import Client
+
+
+def zarrinpal_request_handler(amount, description, user_email, user_phone_number, REQUEST_URL, MERCHANT_ID, CALL_BACK):
+ client = Client(REQUEST_URL)
+ result = client.service.PaymentRequest(
+ MERCHANT_ID, amount, description, user_email, user_phone_number, CALL_BACK
+ )
+ if result.Status == 100:
+ return 'https://sandbox.zarinpal.com/pg/StartPay/' + result.Authority, result.Authority
+ else:
+ return None, None
+
+
+def zarrinpal_payment_verify(amount, authority, REQUEST_URL, MERCHANT_ID,):
+ client = Client(REQUEST_URL)
+ result = client.service.PaymentVerification(MERCHANT_ID, authority, amount)
+ is_paid = True if result.Status in (100, 101) else False
+ return is_paid, result.RefID
diff --git a/gateway/views.py b/gateway/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/gateway/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/item/__init__.py b/item/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/item/admin.py b/item/admin.py
new file mode 100644
index 0000000..c7adcca
--- /dev/null
+++ b/item/admin.py
@@ -0,0 +1,17 @@
+from django.contrib import admin
+
+from item.models import Item, ItemLine
+
+
+class ItemLineInline(admin.TabularInline):
+ model = ItemLine
+ max_num = 1
+
+
+@admin.register(Item)
+class ItemAdmin(admin.ModelAdmin):
+ list_display = ('name', 'upc', 'service', 'category', 'price', 'stock', 'available')
+ list_editable = ('available',)
+ list_filter = ('available', 'created_time')
+ search_fields = ('upc', 'name')
+ inlines = [ItemLineInline]
\ No newline at end of file
diff --git a/item/apps.py b/item/apps.py
new file mode 100644
index 0000000..f290658
--- /dev/null
+++ b/item/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ItemConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'item'
diff --git a/item/forms.py b/item/forms.py
new file mode 100644
index 0000000..d2595db
--- /dev/null
+++ b/item/forms.py
@@ -0,0 +1,19 @@
+from django import forms
+
+from item.models import Item
+
+
+class ItemCreateForm(forms.ModelForm):
+ quantity = forms.IntegerField(initial=0)
+
+ class Meta:
+ model = Item
+ fields = ('name', 'price', 'description', 'available', 'image')
+
+
+class ItemUpdateForm(forms.ModelForm):
+ quantity = forms.IntegerField(initial=0)
+
+ class Meta:
+ model = Item
+ fields = ('category', 'name', 'price', 'description', 'available', 'image')
diff --git a/item/migrations/0001_initial.py b/item/migrations/0001_initial.py
new file mode 100644
index 0000000..7c50bbb
--- /dev/null
+++ b/item/migrations/0001_initial.py
@@ -0,0 +1,52 @@
+# Generated by Django 3.2 on 2021-08-28 20:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('service', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Item',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('upc', models.BigIntegerField(db_index=True, unique=True, verbose_name='upc')),
+ ('available', models.BooleanField(default=True, verbose_name='available')),
+ ('name', models.CharField(max_length=50, verbose_name='name')),
+ ('description', models.TextField(blank=True, verbose_name='description')),
+ ('price', models.IntegerField(verbose_name='price')),
+ ('image', models.ImageField(blank=True, null=True, upload_to='items/', verbose_name='image')),
+ ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='service.servicecategory', verbose_name='category')),
+ ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='service.service', verbose_name='service')),
+ ],
+ options={
+ 'verbose_name': 'Item',
+ 'verbose_name_plural': 'Items',
+ 'db_table': 'item',
+ },
+ ),
+ migrations.CreateModel(
+ name='ItemLine',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('quantity', models.PositiveIntegerField(default=0, verbose_name='quantity')),
+ ('item', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='line', to='item.item', verbose_name='item')),
+ ],
+ options={
+ 'verbose_name': 'Item line',
+ 'verbose_name_plural': 'Item lines',
+ 'db_table': 'item_line',
+ },
+ ),
+ ]
diff --git a/item/migrations/0002_alter_item_upc.py b/item/migrations/0002_alter_item_upc.py
new file mode 100644
index 0000000..c09abd8
--- /dev/null
+++ b/item/migrations/0002_alter_item_upc.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2 on 2021-09-01 08:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('item', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='item',
+ name='upc',
+ field=models.BigIntegerField(db_index=True, editable=False, unique=True, verbose_name='upc'),
+ ),
+ ]
diff --git a/item/migrations/__init__.py b/item/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/item/models.py b/item/models.py
new file mode 100644
index 0000000..c431000
--- /dev/null
+++ b/item/models.py
@@ -0,0 +1,62 @@
+import random
+import string
+
+from django.db import models
+from django.urls import reverse_lazy
+from django.utils.translation import gettext as _
+
+from library.models import BaseModel
+from service.models import Service, ServiceCategory
+
+
+class ItemManager(models.Manager):
+ def available(self):
+ return self.get_queryset().filter(available=True)
+
+
+class Item(BaseModel):
+ upc = models.BigIntegerField(verbose_name=_('upc'), unique=True, db_index=True, editable=False)
+ available = models.BooleanField(verbose_name=_('available'), default=True)
+ name = models.CharField(verbose_name=_('name'), max_length=50)
+ description = models.TextField(verbose_name=_('description'), blank=True)
+ price = models.IntegerField(verbose_name=_('price'))
+ image = models.ImageField(verbose_name=_('image'), blank=True, null=True, upload_to='items/')
+ service = models.ForeignKey(Service, verbose_name=_('service'), related_name='items', on_delete=models.CASCADE)
+ category = models.ForeignKey(ServiceCategory, verbose_name=_('category'), related_name='items',
+ on_delete=models.CASCADE)
+ objects = ItemManager()
+
+ class Meta:
+ verbose_name = _('Item')
+ verbose_name_plural = _('Items')
+ db_table = 'item'
+
+ @property
+ def stock(self):
+ return self.line.quantity
+
+ def __str__(self):
+ return self.name
+
+ def save(self, force_insert=False, force_update=False, using=None,
+ update_fields=None):
+ if self.pk is None: # only when we the instance is created
+ random_digits = ''.join(random.choice(string.digits) for _ in range(5))
+ self.upc = int(random_digits)
+ return super().save(force_insert, force_update, using, update_fields)
+
+ def get_absolute_url(self):
+ return reverse_lazy("item:detail", kwargs={'pk': self.pk})
+
+
+class ItemLine(BaseModel):
+ item = models.OneToOneField(Item, verbose_name=_('item'), related_name='line', on_delete=models.CASCADE)
+ quantity = models.PositiveIntegerField(default=0, verbose_name=_('quantity'))
+
+ class Meta:
+ verbose_name = _('Item line')
+ verbose_name_plural = _('Item lines')
+ db_table = 'item_line'
+
+ def __str__(self):
+ return f"{self.quantity}"
diff --git a/item/templates/item/create_form.html b/item/templates/item/create_form.html
new file mode 100644
index 0000000..4a7f9b9
--- /dev/null
+++ b/item/templates/item/create_form.html
@@ -0,0 +1,22 @@
+{% extends 'base.html' %}
+{% block title %} Add item {% endblock %}
+{% block content %}
+ Add new item
+
+{% endblock %}
\ No newline at end of file
diff --git a/item/templates/item/delete.html b/item/templates/item/delete.html
new file mode 100644
index 0000000..8e62538
--- /dev/null
+++ b/item/templates/item/delete.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/item/templates/item/detail.html b/item/templates/item/detail.html
new file mode 100644
index 0000000..2773c38
--- /dev/null
+++ b/item/templates/item/detail.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% block title %} {{ item.name }} {% endblock %}
+{% block content %}
+ {{ item.name }}
+ Category : {{ item.category.name }}
+ {% if item.image %}
+
+ {% endif %}
+ Description : {{ item.description }}
+ Price : {{ item.price }}
+ Add to cart
+ Back to List
+{% endblock %}
\ No newline at end of file
diff --git a/item/templates/item/item_list.html b/item/templates/item/item_list.html
new file mode 100644
index 0000000..37d1e3b
--- /dev/null
+++ b/item/templates/item/item_list.html
@@ -0,0 +1,194 @@
+{% extends 'base.html' %}
+{% load category_tags cart_tags %}
+{% block banner %}
+
+
+
+ {% if service.banner %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+ {% if service.logo %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
{{ service.name.title }} {{ service.get_service_type_display.title }}
+
Address : {{ service.address }}
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+
+ {% for category in service.categories.all %}
+
+
{{ category.name.title }}
+ {% filter_category items category as category_items %}
+ {% for item in category_items %}
+
+
+
+ {% if item.image %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
{{ item.description | truncatechars:35 }}
+
+
+
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+ {% get_cart request service as cart %}
+ {% if cart %}
+
+
+
+
Cart
+
+
+
+
+
+
+ {% for line in cart.lines.all %}
+
+
+
{{ line.item.name }}
+
{{ line.price }} $
+
+
+
{{ line.quantity }}
+
+ {% if line.quantity == 1 %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+ {% endfor %}
+
+
+
+
+ {% calculate_total_price cart as total_price %}
+
+ Total price
+ {{ total_price }} $
+
+
+
+
check out
+ {% else %}
+
+
+
+
Cart is empty!
+
+
+
+ {% endif %}
+
+
+
+
+
+{% endblock %}
diff --git a/item/templates/item/service_provider_detail.html b/item/templates/item/service_provider_detail.html
new file mode 100644
index 0000000..df0282b
--- /dev/null
+++ b/item/templates/item/service_provider_detail.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% block title %} {{ item.name }} {% endblock %}
+{% block content %}
+ {{ item.name }}
+ {% if item.image %}
+
+ {% endif %}
+ Stock : {{ item.stock }}
+ Category : {{ item.category.name }}
+ Description : {{ item.description }}
+ Price : {{ item.price }}
+ Back to List
+{% endblock %}
\ No newline at end of file
diff --git a/item/templates/item/service_provider_item_list.html b/item/templates/item/service_provider_item_list.html
new file mode 100644
index 0000000..64fe1a6
--- /dev/null
+++ b/item/templates/item/service_provider_item_list.html
@@ -0,0 +1,38 @@
+{% extends 'base.html' %}
+
+{% block content %}
+ Category Detail
+ Category Name : {{ category.name }}
+
+ Items
+
+ Add item
+
+
+ {% for item in items %}
+
+
+
+ {% if item.image %}
+
+ {% else %}
+
+ {% endif %}
+
{{ item.name }}
+
+
Price : {{ item.price }}
+
{{ item.description | truncatechars:30 }}
+
Detail
+
Update
+
Delete
+
+
+
+ {% endfor %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/item/templates/item/update_form.html b/item/templates/item/update_form.html
new file mode 100644
index 0000000..527caf6
--- /dev/null
+++ b/item/templates/item/update_form.html
@@ -0,0 +1,22 @@
+{% extends 'base.html' %}
+{% block title %} Update Item {% endblock %}
+{% block content %}
+ Update item
+
+{% endblock %}
\ No newline at end of file
diff --git a/item/templatetags/__init__.py b/item/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/item/templatetags/category_tags.py b/item/templatetags/category_tags.py
new file mode 100644
index 0000000..b51e656
--- /dev/null
+++ b/item/templatetags/category_tags.py
@@ -0,0 +1,8 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def filter_category(queryset, category):
+ return queryset.filter(category=category)
diff --git a/item/tests.py b/item/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/item/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/item/urls.py b/item/urls.py
new file mode 100644
index 0000000..42557e7
--- /dev/null
+++ b/item/urls.py
@@ -0,0 +1,17 @@
+from django.urls import path
+from item.views import ItemUpdateView, ItemDetailView, ItemDeleteView, ServiceProviderItemDetailView, ItemCreateView, \
+ ItemListView, ServiceCategoryItemListView
+
+app_name = 'item'
+
+urlpatterns = [
+ path('create///', ItemCreateView.as_view(), name='create'),
+ path('update//', ItemUpdateView.as_view(), name='update'),
+ path('detail//', ItemDetailView.as_view(), name='detail'),
+ path('detail-serviceprovider//', ServiceProviderItemDetailView.as_view(), name='detail-service-provider'),
+ path('delete//', ItemDeleteView.as_view(), name='delete'),
+ path('/list/', ItemListView.as_view(), name='list'),
+ path(
+ 'serviceprovider///list/', ServiceCategoryItemListView.as_view(),
+ name='service-provider-list'),
+]
diff --git a/item/views.py b/item/views.py
new file mode 100644
index 0000000..2e57885
--- /dev/null
+++ b/item/views.py
@@ -0,0 +1,156 @@
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404
+from django.urls import reverse_lazy
+from django.utils.decorators import method_decorator
+from django.views.generic import FormView, UpdateView, DetailView, DeleteView, ListView
+
+from accounts.utils import IsServiceProvider
+from item.forms import ItemCreateForm, ItemUpdateForm
+from item.models import Item, ItemLine
+from service.models import Service, ServiceCategory
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ItemCreateView(IsServiceProvider, FormView):
+ model = Item
+ form_class = ItemCreateForm
+ template_name = 'item/create_form.html'
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk'])
+ self.kwargs['category'] = get_object_or_404(ServiceCategory, id=self.kwargs['category_pk'])
+
+ def form_valid(self, form):
+ item = form.save(commit=False)
+ item.service = self.kwargs['service']
+ item.category = self.kwargs['category']
+ item.save()
+ ItemLine.objects.create(item=item, quantity=form.cleaned_data['quantity'])
+ item.save()
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return reverse_lazy('item:service-provider-list',
+ kwargs={'service_pk': self.kwargs['service'].pk, 'category_pk': self.kwargs['category'].pk}
+ )
+
+ def test_func(self):
+ result = super().test_func()
+ service_check = self.kwargs['service'].service_provider == self.request.user
+ return result and service_check
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ItemUpdateView(IsServiceProvider, UpdateView):
+ model = Item
+ template_name = 'item/update_form.html'
+ form_class = ItemUpdateForm
+
+ def get_initial(self):
+ initial = super().get_initial()
+ initial['quantity'] = self.get_object().stock
+ return initial
+
+ def get_form_class(self):
+ form = super().get_form_class()
+ queryset = form.base_fields['category'].queryset
+ form.base_fields['category'].queryset = queryset.filter(service=self.get_object().service)
+ return form
+
+ def form_valid(self, form):
+ item = form.save(commit=False)
+ item.line.quantity = form.cleaned_data['quantity']
+ item.line.save()
+ item.save()
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return reverse_lazy('item:detail-service-provider', kwargs={'pk': self.object.pk})
+
+ def test_func(self):
+ result = super().test_func()
+ return result and self.get_object().service.service_provider == self.request.user
+
+
+class ItemDetailView(DetailView):
+ model = Item
+ template_name = 'item/detail.html'
+ context_object_name = 'item'
+
+ def get_queryset(self):
+ return Item.objects.available()
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderItemDetailView(IsServiceProvider, DetailView):
+ model = Item
+ template_name = 'item/service_provider_detail.html'
+ context_object_name = 'item'
+
+ def test_func(self):
+ result = super().test_func()
+ return result and self.get_object().service.service_provider == self.request.user
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ItemDeleteView(IsServiceProvider, DeleteView):
+ model = Item
+ template_name = 'item/delete.html'
+
+ def test_func(self):
+ result = super().test_func()
+ return result and self.get_object().service.service_provider == self.request.user
+
+ def get_success_url(self):
+ return reverse_lazy('item:service-provider-list',
+ kwargs={'service_pk': self.object.service.pk, 'category_pk': self.object.category.pk}
+ )
+
+
+class BaseItemListView(ListView):
+ model = Item
+ context_object_name = 'items'
+
+
+class ItemListView(BaseItemListView):
+ template_name = 'item/item_list.html'
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk'])
+
+ def get_queryset(self):
+ return Item.objects.available().filter(service=self.kwargs['service'])
+
+ def get_context_data(self, *, object_list=None, **kwargs):
+ context = super().get_context_data(object_list=None, **kwargs)
+ context['service'] = self.kwargs['service']
+ return context
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceCategoryItemListView(IsServiceProvider, BaseItemListView):
+ """
+ This view is for service-provider panel
+ """
+ template_name = 'item/service_provider_item_list.html'
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk'])
+ self.kwargs['category'] = get_object_or_404(ServiceCategory, id=self.kwargs['category_pk'],
+ service=self.kwargs['service'])
+
+ def get_queryset(self):
+ return Item.objects.filter(service=self.kwargs['service'], category=self.kwargs['category'])
+
+ def test_func(self):
+ result = super().test_func()
+ service_check = self.kwargs['service'].service_provider == self.request.user
+ return result and service_check
+
+ def get_context_data(self, *, object_list=None, **kwargs):
+ context = super().get_context_data(object_list=None, **kwargs)
+ context['category'] = self.kwargs['category']
+ return context
diff --git a/library/__init__.py b/library/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/library/models.py b/library/models.py
new file mode 100644
index 0000000..c3fe5f6
--- /dev/null
+++ b/library/models.py
@@ -0,0 +1,9 @@
+from django.db import models
+
+
+class BaseModel(models.Model):
+ created_time = models.DateTimeField(verbose_name='created time', auto_now_add=True)
+ modified_time = models.DateTimeField(verbose_name='modified time', auto_now=True)
+
+ class Meta:
+ abstract = True
diff --git a/library/utils.py b/library/utils.py
new file mode 100644
index 0000000..98b8df5
--- /dev/null
+++ b/library/utils.py
@@ -0,0 +1,28 @@
+from abc import ABC
+
+from django.contrib.auth.mixins import UserPassesTestMixin
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponseRedirect
+
+
+class CustomUserPasses(ABC, UserPassesTestMixin):
+ raise_exception = True
+
+ def handle_no_permission(self, path_url=None, redirect=False):
+ if redirect:
+ return HttpResponseRedirect(path_url)
+
+ else:
+ raise PermissionDenied(self.get_permission_denied_message())
+
+ def dispatch(self, request, *args, **kwargs):
+ user_test_result = self.test_func()
+
+ if isinstance(self.test_func(), tuple):
+ user_test_bool, redirect, path_url = user_test_result
+ if not user_test_bool:
+ return self.handle_no_permission(path_url, redirect)
+
+ if not user_test_result:
+ return self.handle_no_permission()
+ return super().dispatch(request, *args, **kwargs)
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..d465c22
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/order/__init__.py b/order/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/order/admin.py b/order/admin.py
new file mode 100644
index 0000000..e78a395
--- /dev/null
+++ b/order/admin.py
@@ -0,0 +1,11 @@
+from django.contrib import admin
+
+from .models import Order
+
+
+@admin.register(Order)
+class OrderAdmin(admin.ModelAdmin):
+ list_display = ('customer', 'status')
+ list_filter = ('status',)
+ list_editable = ('status',)
+ search_fields = ('customer__phone_number',)
diff --git a/order/apps.py b/order/apps.py
new file mode 100644
index 0000000..42888e4
--- /dev/null
+++ b/order/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class OrderConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'order'
diff --git a/order/filters.py b/order/filters.py
new file mode 100644
index 0000000..12d04cc
--- /dev/null
+++ b/order/filters.py
@@ -0,0 +1,17 @@
+from django import forms
+
+import django_filters
+
+from order.models import Order
+
+
+class OrderFilter(django_filters.FilterSet):
+ created_time = django_filters.DateFilter(
+ widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date', 'data-date-format': 'YYYY-MMMM-DD '}),
+ lookup_expr='contains'
+ )
+
+ status = django_filters.ChoiceFilter(
+ choices=Order.STATUS,
+ widget=forms.Select(attrs={'class': 'form-control'})
+ )
\ No newline at end of file
diff --git a/order/migrations/0001_initial.py b/order/migrations/0001_initial.py
new file mode 100644
index 0000000..a389fbb
--- /dev/null
+++ b/order/migrations/0001_initial.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.2 on 2021-08-28 20:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('payment', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Order',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('status', models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending')], verbose_name='status')),
+ ('is_delivered', models.BooleanField(default=False, verbose_name='is delivered')),
+ ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='payment.invoice', verbose_name='invoice')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/order/migrations/0002_auto_20210913_1544.py b/order/migrations/0002_auto_20210913_1544.py
new file mode 100644
index 0000000..9743ecf
--- /dev/null
+++ b/order/migrations/0002_auto_20210913_1544.py
@@ -0,0 +1,40 @@
+# Generated by Django 3.2 on 2021-09-13 11:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0002_auto_20210831_0046'),
+ ('payment', '0003_auto_20210912_1657'),
+ ('order', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='order',
+ options={'verbose_name': 'Order', 'verbose_name_plural': 'Orders'},
+ ),
+ migrations.AddField(
+ model_name='order',
+ name='customer',
+ field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='accounts.customer', verbose_name='customer'),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='invoice',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='order', to='payment.invoice', verbose_name='invoice'),
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='status',
+ field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending')], default=0, verbose_name='status'),
+ ),
+ migrations.AlterModelTable(
+ name='order',
+ table='order',
+ ),
+ ]
diff --git a/order/migrations/0003_alter_order_status.py b/order/migrations/0003_alter_order_status.py
new file mode 100644
index 0000000..cf7f259
--- /dev/null
+++ b/order/migrations/0003_alter_order_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2 on 2021-09-15 09:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('order', '0002_auto_20210913_1544'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='order',
+ name='status',
+ field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (1, 'sending')], default=0, verbose_name='status'),
+ ),
+ ]
diff --git a/order/migrations/0003_auto_20210913_1956.py b/order/migrations/0003_auto_20210913_1956.py
new file mode 100644
index 0000000..40915c8
--- /dev/null
+++ b/order/migrations/0003_auto_20210913_1956.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.2 on 2021-09-13 15:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('order', '0002_auto_20210913_1544'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='order',
+ name='is_delivered',
+ ),
+ migrations.AlterField(
+ model_name='order',
+ name='status',
+ field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending'), (2, 'delivered')], default=0, verbose_name='status'),
+ ),
+ ]
diff --git a/order/migrations/0004_alter_order_status.py b/order/migrations/0004_alter_order_status.py
new file mode 100644
index 0000000..b345535
--- /dev/null
+++ b/order/migrations/0004_alter_order_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2 on 2021-09-13 15:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('order', '0003_auto_20210913_1956'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='order',
+ name='status',
+ field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (2, 'sending'), (2, 'delivered')], default=0, verbose_name='status'),
+ ),
+ ]
diff --git a/order/migrations/0005_alter_order_status.py b/order/migrations/0005_alter_order_status.py
new file mode 100644
index 0000000..7a4bb6a
--- /dev/null
+++ b/order/migrations/0005_alter_order_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2 on 2021-09-13 15:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('order', '0004_alter_order_status'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='order',
+ name='status',
+ field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (1, 'sending'), (2, 'delivered')], default=0, verbose_name='status'),
+ ),
+ ]
diff --git a/order/migrations/0006_merge_0003_alter_order_status_0005_alter_order_status.py b/order/migrations/0006_merge_0003_alter_order_status_0005_alter_order_status.py
new file mode 100644
index 0000000..d0217ad
--- /dev/null
+++ b/order/migrations/0006_merge_0003_alter_order_status_0005_alter_order_status.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.2 on 2021-09-16 08:59
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('order', '0003_alter_order_status'),
+ ('order', '0005_alter_order_status'),
+ ]
+
+ operations = [
+ ]
diff --git a/order/migrations/__init__.py b/order/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/order/models.py b/order/models.py
new file mode 100644
index 0000000..2079af6
--- /dev/null
+++ b/order/models.py
@@ -0,0 +1,33 @@
+from django.db import models
+
+from accounts.models import Customer
+from library.models import BaseModel
+from django.utils.translation import ugettext_lazy as _
+
+from payment.models import Invoice
+
+
+class Order(BaseModel):
+ PREPARING_FOOD = 0
+ SENDING = 1
+ DELIVERED = 2
+ STATUS = (
+ (PREPARING_FOOD, _('preparing food')),
+ (SENDING, _('sending')),
+ (DELIVERED, _('delivered'))
+ )
+ invoice = models.OneToOneField(Invoice, verbose_name=_('invoice'), related_name='order', on_delete=models.PROTECT)
+ customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='orders', on_delete=models.PROTECT)
+ status = models.PositiveSmallIntegerField(verbose_name=_('status'), choices=STATUS, default=PREPARING_FOOD)
+
+ class Meta:
+ verbose_name = _('Order')
+ verbose_name_plural = _('Orders')
+ db_table = 'order'
+
+ def __str__(self):
+ return f"{self.customer} - {self.status}"
+
+ @classmethod
+ def create(cls, invoice):
+ cls.objects.create(invoice=invoice, customer=invoice.customer)
diff --git a/order/templates/order/customer/order_detail.html b/order/templates/order/customer/order_detail.html
new file mode 100644
index 0000000..d305132
--- /dev/null
+++ b/order/templates/order/customer/order_detail.html
@@ -0,0 +1,38 @@
+{% extends 'base.html' %}
+{% block title %} Order Detail {% endblock %}
+
+{% block content %}
+ {{ order.invoice.cart.service }}
+
+ status : {{ order.get_status_display }}
+ order date time : {{ order.created_time }}
+ address : {{ order.invoice.address.state.name }} - {{ order.invoice.address.city.name }}
+ - {{ order.invoice.address.area.name }} - {{ order.invoice.address.street }}
+ - {{ order.invoice.address.alley }} - {{ order.invoice.address.floor }}
+ - {{ order.invoice.address.plaque }}
+
+
+
+
+
+ Item
+ Quantity
+ Price
+
+
+
+
+ {% for line in order.invoice.cart.lines.all %}
+
+ {{ line.item }}
+ {{ line.quantity }}
+ $ {{ line.price }}
+
+ {% endfor %}
+
+
+
+
+ Total price : {{ order.invoice.price }}
+
+{% endblock %}
\ No newline at end of file
diff --git a/order/templates/order/customer/order_list.html b/order/templates/order/customer/order_list.html
new file mode 100644
index 0000000..3320957
--- /dev/null
+++ b/order/templates/order/customer/order_list.html
@@ -0,0 +1,45 @@
+{% extends 'base.html' %}
+{% block title %} Orders {% endblock %}
+
+{% block content %}
+ Active orders
+ {% if active_orders %}
+
+ {% else %}
+
+ You have no active orders :(
+ {% endif %}
+ Delivered orders
+ {% if delivered_orders %}
+
+ {% else %}
+
+ You have no delivered orders
+ {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/order/templates/order/service/order_detail.html b/order/templates/order/service/order_detail.html
new file mode 100644
index 0000000..1536d92
--- /dev/null
+++ b/order/templates/order/service/order_detail.html
@@ -0,0 +1,55 @@
+{% extends 'base.html' %}
+{% block title %}Order Detail{% endblock %}
+{% block content %}
+
+
+
+
+
+
id : {{ order.id }}
+
status : {{ order.get_status_display }}
+
date time : {{ order.created_time }}
+
total price : {{ order.invoice.price }}$
+
+
+
+
+
Lines:
+
+
+ {% for cartline in order.invoice.cart.lines.all %}
+
+
+
+
+ {{ cartline.item }}
+
+
+
+
+ {{ cartline.quantity }}
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/order/templates/order/service/order_filter_list.html b/order/templates/order/service/order_filter_list.html
new file mode 100644
index 0000000..6701b03
--- /dev/null
+++ b/order/templates/order/service/order_filter_list.html
@@ -0,0 +1,34 @@
+{% extends 'base.html' %}
+{% block title %}Payment Verification{% endblock %}
+{% block content %}
+
+
+
+
+
+
+ {% for order in orders %}
+
+
+
+
+
id : {{ order.id }}
+
status : {{ order.get_status_display }}
+
date time : {{ order.created_time }}
+
total price : {{ order.invoice.price }}$
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/order/templates/order/service/order_list.html b/order/templates/order/service/order_list.html
new file mode 100644
index 0000000..b603793
--- /dev/null
+++ b/order/templates/order/service/order_list.html
@@ -0,0 +1,26 @@
+{% extends 'base.html' %}
+{% block title %}Payment Verification{% endblock %}
+{% block content %}
+ Today: {% now "Y-m-d" %}
+
+ {% for order in orders %}
+
+
+
+
+
id : {{ order.id }}
+
status : {{ order.get_status_display }}
+
date time : {{ order.created_time }}
+
total price : {{ order.invoice.price }}$
+
+
+
+
+
+ {% endfor %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/order/templates/order/service/order_update.html b/order/templates/order/service/order_update.html
new file mode 100644
index 0000000..2d4f93e
--- /dev/null
+++ b/order/templates/order/service/order_update.html
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+{% block content %}
+
+ cancel
+{% endblock %}
diff --git a/order/tests.py b/order/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/order/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/order/urls.py b/order/urls.py
new file mode 100644
index 0000000..f1a4e3e
--- /dev/null
+++ b/order/urls.py
@@ -0,0 +1,16 @@
+from django.urls import path
+
+from order.views import OrderServiceListView, OrderServiceDetailView, OrderServiceUpdateView, OrderServiceListFilterView
+from order.views import CustomerOrdersListView, CustomerOrderDetailView
+
+app_name = 'order'
+
+urlpatterns = [
+ path('service/list//', OrderServiceListFilterView.as_view(), name='service-order-list'),
+ path('service/today//', OrderServiceListView.as_view(), name='service-order-today'),
+ path('service/detail//', OrderServiceDetailView.as_view(), name='service-order-detail'),
+ path('service/update//', OrderServiceUpdateView.as_view(), name='service-order-update'),
+
+ path('customer/list/', CustomerOrdersListView.as_view(), name='customer-list'),
+ path('customer//detail/', CustomerOrderDetailView.as_view(), name='customer-detail'),
+]
\ No newline at end of file
diff --git a/order/views.py b/order/views.py
new file mode 100644
index 0000000..ce952aa
--- /dev/null
+++ b/order/views.py
@@ -0,0 +1,130 @@
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponseForbidden
+from django.shortcuts import get_object_or_404
+from django.urls import reverse_lazy
+from django.utils import timezone
+from django.utils.decorators import method_decorator
+from django.views.decorators.http import require_http_methods
+from django.views.generic import ListView, DetailView, UpdateView
+
+from django_filters.views import FilterView
+
+from order.filters import OrderFilter
+from order.models import Order
+from service.models import Service
+
+from library.utils import CustomUserPasses
+
+from accounts.models import Customer, ServiceProvider
+from accounts.utils import IsCustomer
+
+
+class BaseOrderServiceList(CustomUserPasses):
+ model = Order
+
+ def test_func(self):
+ if not isinstance(self.request.user, ServiceProvider):
+ return False
+ if self.service.service_provider != self.request.user:
+ return False
+ return True
+
+
+class BaseOrderDetailUpdate(CustomUserPasses):
+ model = Order
+
+ def test_func(self):
+ order = self.get_object()
+ if not isinstance(self.reqeust.user, ServiceProvider):
+ return False
+ if order.invoice.cart.service.service_provider != self.request.user:
+ return False
+ return True
+
+
+@method_decorator(require_http_methods(['GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class OrderServiceListFilterView(FilterView):
+ context_object_name = 'orders'
+ filterset_class = OrderFilter
+ template_name = 'order/service/order_filter_list.html'
+
+ def dispatch(self, request, *args, **kwargs):
+ self.service = get_object_or_404(Service, pk=self.kwargs.get('service_pk', None))
+ if self.service.service_provider != self.request.user:
+ return HttpResponseForbidden()
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_queryset(self):
+ return Order.objects.filter(invoice__cart__service=self.service)
+
+
+@method_decorator(require_http_methods(['GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class OrderServiceListView(BaseOrderServiceList, ListView):
+ context_object_name = 'orders'
+ template_name = 'order/service/order_list.html'
+
+ def dispatch(self, request, *args, **kwargs):
+ self.service = get_object_or_404(Service, pk=self.kwargs.get('service_pk', None))
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_queryset(self):
+ today = timezone.now()
+ time_filter = dict(
+ created_time__day=today.strftime('%d'),
+ created_time__month=today.strftime('%m'),
+ created_time__year=today.strftime('%Y')
+ )
+ return Order.objects.exclude(is_delivered=True).filter(invoice__cart__service=self.service, **time_filter)
+
+
+@method_decorator(require_http_methods(['GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class OrderServiceDetailView(BaseOrderDetailUpdate, DetailView):
+ context_object_name = 'order'
+ template_name = 'order/service/order_detail.html'
+
+
+@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class OrderServiceUpdateView(BaseOrderDetailUpdate, UpdateView):
+ fields = ('status',)
+ context_object_name = 'order'
+ template_name = 'order/service/order_update.html'
+
+ def get_success_url(self):
+ order = self.get_object()
+ return reverse_lazy('order:service-order-list', kwargs={'service_pk': order.invoice.cart.service.id})
+
+
+@method_decorator(require_http_methods(['GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerOrdersListView(IsCustomer, ListView):
+ template_name = 'order/customer/order_list.html'
+ context_object_name = 'active_orders'
+
+ def get_queryset(self):
+ return Order.objects.select_related('invoice__cart').filter(customer=self.request.user, status__in=(0, 1))
+
+ def get_context_data(self, *, object_list=None, **kwargs):
+ context = super().get_context_data(object_list=None, **kwargs)
+ context['delivered_orders'] = Order.objects.select_related('invoice__cart').filter(customer=self.request.user,
+ status=2)
+ return context
+
+
+@method_decorator(require_http_methods(['GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CustomerOrderDetailView(CustomUserPasses, DetailView):
+ model = Order
+ template_name = 'order/customer/order_detail.html'
+ context_object_name = 'order'
+
+ def test_func(self):
+ user = self.request.user
+ if not isinstance(user, Customer):
+ return False
+ if self.get_object().customer != self.request.user:
+ return False
+ return True
diff --git a/payment/__init__.py b/payment/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/payment/admin.py b/payment/admin.py
new file mode 100644
index 0000000..d8e582e
--- /dev/null
+++ b/payment/admin.py
@@ -0,0 +1,17 @@
+from django.contrib import admin
+
+from payment.models import Invoice, Payment
+
+
+@admin.register(Invoice)
+class InvoiceAdmin(admin.ModelAdmin):
+ list_display = ('customer', 'price', 'is_paid', 'address')
+ list_filter = ('is_paid', 'created_time')
+ search_fields = ('customer__phone_number',)
+
+
+@admin.register(Payment)
+class PaymentAdmin(admin.ModelAdmin):
+ list_display = ('uuid', 'customer', 'gateway', 'price', 'is_paid')
+ list_filter = ('is_paid', 'created_time', 'gateway')
+ search_fields = ('uuid', 'customer__phone_number')
diff --git a/payment/apps.py b/payment/apps.py
new file mode 100644
index 0000000..b4a45c3
--- /dev/null
+++ b/payment/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PaymentConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'payment'
diff --git a/payment/forms.py b/payment/forms.py
new file mode 100644
index 0000000..f348305
--- /dev/null
+++ b/payment/forms.py
@@ -0,0 +1,12 @@
+from django import forms
+
+from address.models import CustomerAddress
+from gateway.models import Gateway
+
+
+class AddressSelectForm(forms.Form):
+ address = forms.ModelChoiceField(queryset=CustomerAddress.objects.all(), widget=forms.RadioSelect)
+
+
+class GatewaySelectForm(forms.Form):
+ gateway = forms.ModelChoiceField(queryset=Gateway.objects.filter(is_enable=True), widget=forms.RadioSelect)
diff --git a/payment/migrations/0001_initial.py b/payment/migrations/0001_initial.py
new file mode 100644
index 0000000..a281ac2
--- /dev/null
+++ b/payment/migrations/0001_initial.py
@@ -0,0 +1,52 @@
+# Generated by Django 3.2 on 2021-08-28 20:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ('address', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Invoice',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('price', models.IntegerField(verbose_name='price')),
+ ('is_paid', models.BooleanField(default=False, verbose_name='is paid')),
+ ('address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.address', verbose_name='address')),
+ ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='accounts.customer', verbose_name='customer')),
+ ],
+ options={
+ 'verbose_name': 'Invoice',
+ 'verbose_name_plural': 'Invoices',
+ 'db_table': 'invoice',
+ },
+ ),
+ migrations.CreateModel(
+ name='Payment',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True, verbose_name='uuid')),
+ ('price', models.IntegerField(verbose_name='price')),
+ ('is_paid', models.BooleanField(default=False, verbose_name='is paid')),
+ ('invoice', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='payment.invoice', verbose_name='invoice')),
+ ],
+ options={
+ 'verbose_name': 'Payment',
+ 'verbose_name_plural': 'Payments',
+ 'db_table': 'payment',
+ },
+ ),
+ ]
diff --git a/payment/migrations/0002_alter_invoice_address.py b/payment/migrations/0002_alter_invoice_address.py
new file mode 100644
index 0000000..09765ea
--- /dev/null
+++ b/payment/migrations/0002_alter_invoice_address.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2 on 2021-08-30 08:35
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0002_auto_20210830_1305'),
+ ('payment', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='invoice',
+ name='address',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.customeraddress', verbose_name='address'),
+ ),
+ ]
diff --git a/payment/migrations/0003_auto_20210912_1657.py b/payment/migrations/0003_auto_20210912_1657.py
new file mode 100644
index 0000000..f28c2d5
--- /dev/null
+++ b/payment/migrations/0003_auto_20210912_1657.py
@@ -0,0 +1,54 @@
+# Generated by Django 3.2 on 2021-09-12 12:27
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0006_rename_user_customeraddress_customer_user'),
+ ('gateway', '0001_initial'),
+ ('cart', '0002_remove_cart_invoice'),
+ ('accounts', '0002_auto_20210831_0046'),
+ ('payment', '0002_alter_invoice_address'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='invoice',
+ name='cart',
+ field=models.OneToOneField(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='invoice', to='cart.cart', verbose_name='cart'),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='payment',
+ name='authority',
+ field=models.CharField(blank=True, max_length=64, verbose_name='authority'),
+ ),
+ migrations.AddField(
+ model_name='payment',
+ name='customer',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='accounts.customer', verbose_name='customer'),
+ ),
+ migrations.AddField(
+ model_name='payment',
+ name='gateway',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='gateway.gateway', verbose_name='gateway'),
+ ),
+ migrations.AddField(
+ model_name='payment',
+ name='payment_log',
+ field=models.TextField(blank=True, verbose_name='logs'),
+ ),
+ migrations.AlterField(
+ model_name='invoice',
+ name='address',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.customeraddress', verbose_name='address'),
+ ),
+ migrations.AlterField(
+ model_name='payment',
+ name='invoice',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='payment.invoice', verbose_name='invoice'),
+ ),
+ ]
diff --git a/payment/migrations/__init__.py b/payment/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/payment/models.py b/payment/models.py
new file mode 100644
index 0000000..86299c9
--- /dev/null
+++ b/payment/models.py
@@ -0,0 +1,104 @@
+from django.db import models, transaction
+from django.utils.translation import gettext as _
+from accounts.models import Customer
+from address.models import CustomerAddress
+from cart.models import Cart
+from gateway.models import Gateway
+from library.models import BaseModel
+import uuid
+
+from yummy.settings import CALL_BACK
+
+
+class Invoice(BaseModel):
+ customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='invoices',
+ on_delete=models.SET_NULL, null=True)
+ price = models.IntegerField(verbose_name=_('price'))
+ is_paid = models.BooleanField(verbose_name=_('is paid'), default=False)
+ cart = models.OneToOneField(Cart, verbose_name=_('cart'), related_name='invoice', on_delete=models.PROTECT)
+ address = models.ForeignKey(CustomerAddress, verbose_name=_('address'), related_name='invoices',
+ on_delete=models.SET_NULL, null=True)
+
+ class Meta:
+ verbose_name = _('Invoice')
+ verbose_name_plural = _('Invoices')
+ db_table = 'invoice'
+
+ @classmethod
+ def create_payment(cls, invoice, gateway):
+ return Payment.objects.create(invoice=invoice, price=invoice.price, customer=invoice.customer, gateway=gateway)
+
+ @classmethod
+ def create(cls, user, cart, address, gateway):
+ with transaction.atomic():
+ invoice = cls.objects.create(customer=user, cart=cart, price=cart.total_price, address=address)
+ payment = cls.create_payment(invoice=invoice, gateway=gateway)
+
+ return payment
+
+ def __str__(self):
+ return f"{self.customer} - {self.price} - {'Paid' if self.is_paid else 'Not paid'}"
+
+
+class Payment(BaseModel):
+ uuid = models.UUIDField(unique=True, verbose_name=_('uuid'), db_index=True, default=uuid.uuid4)
+ invoice = models.ForeignKey(Invoice, verbose_name=_('invoice'), related_name='payment', on_delete=models.CASCADE)
+ price = models.IntegerField(verbose_name=_('price'))
+ customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='payments',
+ on_delete=models.SET_NULL, null=True)
+ is_paid = models.BooleanField(verbose_name=_('is paid'), default=False)
+ gateway = models.ForeignKey(Gateway, verbose_name=_('gateway'), related_name='payments', on_delete=models.SET_NULL,
+ null=True)
+ payment_log = models.TextField(verbose_name=_('logs'), blank=True)
+ authority = models.CharField(max_length=64, verbose_name=_('authority'), blank=True)
+
+ class Meta:
+ verbose_name = _('Payment')
+ verbose_name_plural = _('Payments')
+ db_table = 'payment'
+
+ def __str__(self):
+ return f"{self.price} - {'Paid' if self.is_paid else 'Not paid'}"
+
+ def get_request_handler_data(self):
+ return dict(
+ amount=self.price, description=self.description, user_email=getattr(self.customer, 'email', None),
+ user_phone_number=self.customer.phone_number, REQUEST_URL=self.gateway.gateway_request_url,
+ MERCHANT_ID=self.gateway.auth_data, CALL_BACK=CALL_BACK,
+ )
+
+ @property
+ def bank_page(self):
+ handler = self.gateway.get_request_handler()
+ if handler:
+ link, authority = handler(**self.get_request_handler_data())
+ if authority:
+ self.authority = authority
+ self.save()
+ return link
+
+ def get_verify_handler_data(self):
+ return dict(
+ amount=self.price, authority=self.authority, REQUEST_URL=self.gateway.gateway_request_url,
+ MERCHANT_ID=self.gateway.auth_data
+ )
+
+ def verify(self):
+ from order.models import Order
+ handler = self.gateway.get_verify_handler()
+ if handler:
+ is_paid, ref_id = handler(**self.get_verify_handler_data())
+ if is_paid:
+ with transaction.atomic():
+ self.is_paid = True
+ self.invoice.is_paid = True
+ self.invoice.cart.is_paid = True
+ self.invoice.cart.save()
+ self.invoice.save()
+ self.save()
+ Order.create(invoice=self.invoice)
+ return is_paid, ref_id
+
+ @property
+ def description(self):
+ return 'buy of snapp food service'
diff --git a/payment/templates/payment/checkout.html b/payment/templates/payment/checkout.html
new file mode 100644
index 0000000..b4c43cb
--- /dev/null
+++ b/payment/templates/payment/checkout.html
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/payment/templates/payment/verify.html b/payment/templates/payment/verify.html
new file mode 100644
index 0000000..b96b425
--- /dev/null
+++ b/payment/templates/payment/verify.html
@@ -0,0 +1,17 @@
+{% extends 'base.html' %}
+{% block title %}Payment Verification{% endblock %}
+{% block content %}
+ {% if is_paid %}
+
+
+
Payment was successful
+ Your ref code: {{ ref_id }}
+
+ {% else %}
+
+
Payment was successful
+ Your ref code: {{ ref_id }}
+
+ {% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/payment/tests.py b/payment/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/payment/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/payment/urls.py b/payment/urls.py
new file mode 100644
index 0000000..e9d8f62
--- /dev/null
+++ b/payment/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path
+
+from payment.views import CheckoutView, PaymentVerify
+
+app_name = 'payment'
+
+urlpatterns = [
+ path('checkout/', CheckoutView.as_view(), name='checkout'),
+ path('verify/', PaymentVerify.as_view(), name='verify'),
+]
diff --git a/payment/views.py b/payment/views.py
new file mode 100644
index 0000000..619e65c
--- /dev/null
+++ b/payment/views.py
@@ -0,0 +1,68 @@
+from django.contrib.auth.decorators import login_required
+from django.db import transaction
+from django.http import HttpResponse, HttpResponseRedirect
+from django.shortcuts import render, get_object_or_404
+from django.urls import reverse_lazy
+from django.utils.decorators import method_decorator
+from django.views import View
+from django.views.decorators.http import require_http_methods
+
+from address.models import CustomerAddress
+from cart.models import Cart
+from library.utils import CustomUserPasses
+from payment.forms import AddressSelectForm, GatewaySelectForm
+from payment.models import Payment, Invoice
+
+
+@method_decorator(require_http_methods(['POST', 'GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class CheckoutView(CustomUserPasses, View):
+
+ def test_func(self):
+ if not self.cart.lines.exists():
+ return False
+ return True
+
+ def dispatch(self, request, *args, **kwargs):
+ cart_id = self.request.COOKIES.get('cart_id', None)
+ self.cart = get_object_or_404(Cart, pk=cart_id)
+ if self.cart.customer is None:
+ self.cart.customer = request.user
+ self.cart.save()
+
+ return super().dispatch(request, *args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ address_form = AddressSelectForm()
+ address_form.base_fields['address'].queyset = CustomerAddress.objects.filter(customer_user=request.user)
+ gateway_form = GatewaySelectForm()
+ return render(request, 'payment/checkout.html',
+ context={'address_form': address_form, 'gateway_form': gateway_form}
+ )
+
+ def post(self, request, *args, **kwargs):
+ address_form = AddressSelectForm(request.POST)
+ gateway_form = GatewaySelectForm(request.POST)
+
+ if address_form.is_valid() and gateway_form.is_valid():
+ address = address_form.cleaned_data['address']
+ gateway = gateway_form.cleaned_data['gateway']
+ with transaction.atomic():
+ payment = Invoice.create(request.user, self.cart, address=address, gateway=gateway)
+ bank_url = payment.bank_page
+
+ return HttpResponseRedirect(bank_url)
+ return self.get(request, *args, **kwargs)
+
+
+@method_decorator(require_http_methods(['GET']), name='dispatch')
+@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch')
+class PaymentVerify(View):
+ def get(self, request, *args, **kwargs):
+ authority = self.request.GET.get('Authority', None)
+ if authority:
+ payment = get_object_or_404(Payment, authority=authority)
+ is_paid, ref_id = payment.verify()
+ response = render(request, 'payment/verify.html', {'is_paid': is_paid, 'ref_id': ref_id})
+ response.delete_cookie('cart_id')
+ return response
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..96437c1
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+asgiref==3.4.1
+Django==3.2
+django-filter==2.4.0
+Pillow==8.3.1
+psycopg2-binary==2.9.1
+pytz==2021.1
+sqlparse==0.4.1
+suds-jurko==0.6
diff --git a/service/__init__.py b/service/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/service/admin.py b/service/admin.py
new file mode 100644
index 0000000..bd061c4
--- /dev/null
+++ b/service/admin.py
@@ -0,0 +1,30 @@
+from django.contrib import admin
+from .models import Service, ServiceCategory, DeliveryArea, ServiceAvailableTime
+
+
+@admin.register(Service)
+class ServiceAdmin(admin.ModelAdmin):
+ list_display = ('uuid', 'name', 'service_type', 'minimum_purchase', 'address')
+ list_filter = ('service_type',)
+ search_fields = ('name',)
+
+
+@admin.register(ServiceCategory)
+class ServiceCategoryAdmin(admin.ModelAdmin):
+ list_display = ('name', 'slug', 'service')
+ search_fields = ('name', 'service')
+
+
+@admin.register(DeliveryArea)
+class DeliveryAreaAdmin(admin.ModelAdmin):
+ list_display = ('service', 'area')
+ search_fields = ('service', 'area')
+
+
+@admin.register(ServiceAvailableTime)
+class ServiceAvailableTimeAdmin(admin.ModelAdmin):
+ list_display = ('service', 'day', 'open_time', 'close_time', 'is_close')
+ list_filter = ('day', 'is_close')
+ search_fields = ('service',)
+
+
diff --git a/service/apps.py b/service/apps.py
new file mode 100644
index 0000000..8d0ae67
--- /dev/null
+++ b/service/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ServiceConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'service'
diff --git a/service/filters.py b/service/filters.py
new file mode 100644
index 0000000..41920a9
--- /dev/null
+++ b/service/filters.py
@@ -0,0 +1,16 @@
+from django import forms
+
+import django_filters
+
+from service.models import Service
+
+
+class ServiceFilter(django_filters.FilterSet):
+ name = django_filters.CharFilter(
+ widget=forms.TextInput(attrs={'class': 'form-control'}), lookup_expr='icontains'
+ )
+
+ service_type = django_filters.ChoiceFilter(
+ choices=Service.SERVICE_TYPES,
+ widget=forms.Select(attrs={'class': 'form-control'})
+ )
diff --git a/service/forms.py b/service/forms.py
new file mode 100644
index 0000000..7b55c5e
--- /dev/null
+++ b/service/forms.py
@@ -0,0 +1,43 @@
+from django import forms
+
+from service.models import Service, ServiceCategory, DeliveryArea, ServiceAvailableTime
+
+
+class ServiceCreateUpdateForm(forms.ModelForm):
+ class Meta:
+ model = Service
+ fields = ('name', 'service_type', 'minimum_purchase', 'available', 'logo', 'banner')
+ widgets = {
+ 'name': forms.TextInput(attrs={'class': 'form-control'}),
+ 'service_type': forms.Select(attrs={'class': 'form-control'}),
+ 'minimum_purchase': forms.NumberInput(attrs={'class': 'form-control'}),
+ 'logo': forms.FileInput(attrs={'class': 'form-control'}),
+ 'banner': forms.FileInput(attrs={'class': 'form-control'}),
+ 'available': forms.NullBooleanSelect(attrs={'class': 'form-control'}),
+ }
+
+
+class ServiceCategoryCreateUpdateForm(forms.ModelForm):
+ class Meta:
+ model = ServiceCategory
+ fields = ('name',)
+ widgets = {'name': forms.TextInput(attrs={'class': 'form-control'})}
+
+
+class DeliveryAreaCreateUpdateForm(forms.ModelForm):
+ class Meta:
+ model = DeliveryArea
+ fields = ('area',)
+ widgets = {'area': forms.Select(attrs={'class': 'form-control'})}
+
+
+class ServiceAvailableTimeCreateUpdateForm(forms.ModelForm):
+ class Meta:
+ model = ServiceAvailableTime
+ fields = ('day', 'open_time', 'close_time', 'is_close')
+ widgets = {
+ 'day': forms.Select(attrs={'class': 'form-control'}),
+ 'open_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
+ 'close_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
+ 'is_close': forms.NullBooleanSelect(attrs={'class': 'form-control'})
+ }
diff --git a/service/migrations/0001_initial.py b/service/migrations/0001_initial.py
new file mode 100644
index 0000000..307ff2b
--- /dev/null
+++ b/service/migrations/0001_initial.py
@@ -0,0 +1,100 @@
+# Generated by Django 3.2 on 2021-08-28 20:31
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ('address', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='AvailableTime',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('day', models.PositiveSmallIntegerField(choices=[(0, 'saturday'), (1, 'sunday'), (2, 'monday'), (3, 'tuesday'), (4, 'wednesday'), (5, 'thursday'), (6, 'friday')], verbose_name='day')),
+ ('open_time', models.DateTimeField(verbose_name='open time')),
+ ('close_time', models.DateTimeField(verbose_name='close time')),
+ ('close_day', models.BooleanField(blank=True, null=True, verbose_name='close day')),
+ ],
+ options={
+ 'verbose_name': 'AvailableTime',
+ 'verbose_name_plural': 'AvailableTimes',
+ 'db_table': 'available_time',
+ },
+ ),
+ migrations.CreateModel(
+ name='Service',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True, verbose_name='uuid')),
+ ('name', models.CharField(max_length=40, verbose_name='name')),
+ ('service_type', models.PositiveSmallIntegerField(choices=[(0, 'restaurant'), (1, 'cafe'), (2, 'confectionery'), (3, 'supermarket')], verbose_name='service type')),
+ ('minimum_purchase', models.DecimalField(decimal_places=0, max_digits=9, verbose_name='minimum purchase')),
+ ('address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.address', verbose_name='address')),
+ ('service_provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='accounts.serviceprovider', verbose_name='service provider')),
+ ],
+ options={
+ 'verbose_name': 'Service',
+ 'verbose_name_plural': 'Services',
+ 'db_table': 'service',
+ },
+ ),
+ migrations.CreateModel(
+ name='ServiceCategory',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('name', models.CharField(max_length=30, verbose_name='name')),
+ ('slug', models.SlugField(allow_unicode=True, max_length=35, verbose_name='slug')),
+ ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='service.service', verbose_name='service')),
+ ],
+ options={
+ 'verbose_name': 'ServiceCategory',
+ 'verbose_name_plural': 'ServiceCategories',
+ 'db_table': 'service_category',
+ },
+ ),
+ migrations.CreateModel(
+ name='ServiceAvailableTime',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('available_time', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='service.availabletime', verbose_name='available time')),
+ ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='available_times', to='service.service', verbose_name='service')),
+ ],
+ options={
+ 'verbose_name': 'ServiceAvailableTime',
+ 'verbose_name_plural': 'ServiceAvailableTimes',
+ 'db_table': 'service_available_time',
+ },
+ ),
+ migrations.CreateModel(
+ name='DeliveryArea',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
+ ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')),
+ ('area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='delivery_areas', to='address.area', verbose_name='area')),
+ ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='delivery_areas', to='service.service', verbose_name='service')),
+ ],
+ options={
+ 'verbose_name': 'DeliveryArea',
+ 'verbose_name_plural': 'DeliveryAreas',
+ 'db_table': 'delivery_area',
+ },
+ ),
+ ]
diff --git a/service/migrations/0002_alter_service_address.py b/service/migrations/0002_alter_service_address.py
new file mode 100644
index 0000000..d26148c
--- /dev/null
+++ b/service/migrations/0002_alter_service_address.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2 on 2021-08-30 08:35
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0002_auto_20210830_1305'),
+ ('service', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='service',
+ name='address',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'),
+ ),
+ ]
diff --git a/service/migrations/0003_alter_service_address.py b/service/migrations/0003_alter_service_address.py
new file mode 100644
index 0000000..972ed47
--- /dev/null
+++ b/service/migrations/0003_alter_service_address.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2 on 2021-08-31 11:56
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0006_rename_user_customeraddress_customer_user'),
+ ('service', '0002_alter_service_address'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='service',
+ name='address',
+ field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'),
+ ),
+ ]
diff --git a/service/migrations/0004_alter_service_address.py b/service/migrations/0004_alter_service_address.py
new file mode 100644
index 0000000..04a0d58
--- /dev/null
+++ b/service/migrations/0004_alter_service_address.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2 on 2021-08-31 11:58
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('address', '0006_rename_user_customeraddress_customer_user'),
+ ('service', '0003_alter_service_address'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='service',
+ name='address',
+ field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'),
+ ),
+ ]
diff --git a/service/migrations/0005_auto_20210902_1522.py b/service/migrations/0005_auto_20210902_1522.py
new file mode 100644
index 0000000..61db266
--- /dev/null
+++ b/service/migrations/0005_auto_20210902_1522.py
@@ -0,0 +1,40 @@
+# Generated by Django 3.2 on 2021-09-02 10:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('service', '0004_alter_service_address'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='serviceavailabletime',
+ name='available_time',
+ ),
+ migrations.AddField(
+ model_name='serviceavailabletime',
+ name='close_time',
+ field=models.TimeField(null=True, verbose_name='close time'),
+ ),
+ migrations.AddField(
+ model_name='serviceavailabletime',
+ name='day',
+ field=models.PositiveSmallIntegerField(choices=[(0, 'saturday'), (1, 'sunday'), (2, 'monday'), (3, 'tuesday'), (4, 'wednesday'), (5, 'thursday'), (6, 'friday')], null=True, verbose_name='day'),
+ ),
+ migrations.AddField(
+ model_name='serviceavailabletime',
+ name='is_close',
+ field=models.BooleanField(blank=True, null=True, verbose_name='close day'),
+ ),
+ migrations.AddField(
+ model_name='serviceavailabletime',
+ name='open_time',
+ field=models.TimeField(null=True, verbose_name='open time'),
+ ),
+ migrations.DeleteModel(
+ name='AvailableTime',
+ ),
+ ]
diff --git a/service/migrations/0006_auto_20210902_1943.py b/service/migrations/0006_auto_20210902_1943.py
new file mode 100644
index 0000000..c037678
--- /dev/null
+++ b/service/migrations/0006_auto_20210902_1943.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.2 on 2021-09-02 15:13
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('service', '0005_auto_20210902_1522'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='serviceavailabletime',
+ options={'ordering': ('day',), 'verbose_name': 'ServiceAvailableTime', 'verbose_name_plural': 'ServiceAvailableTimes'},
+ ),
+ migrations.AddField(
+ model_name='service',
+ name='banner',
+ field=models.ImageField(blank=True, null=True, upload_to='service/banner', verbose_name='banner'),
+ ),
+ migrations.AddField(
+ model_name='service',
+ name='logo',
+ field=models.ImageField(blank=True, null=True, upload_to='service/logo', verbose_name='logo'),
+ ),
+ ]
diff --git a/service/migrations/0007_auto_20210902_2056.py b/service/migrations/0007_auto_20210902_2056.py
new file mode 100644
index 0000000..d32e53b
--- /dev/null
+++ b/service/migrations/0007_auto_20210902_2056.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.2 on 2021-09-02 16:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('service', '0006_auto_20210902_1943'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='service',
+ options={'ordering': ('created_time',), 'verbose_name': 'Service', 'verbose_name_plural': 'Services'},
+ ),
+ migrations.AlterField(
+ model_name='service',
+ name='banner',
+ field=models.ImageField(blank=True, null=True, upload_to='service/banners/', verbose_name='banner'),
+ ),
+ migrations.AlterField(
+ model_name='service',
+ name='logo',
+ field=models.ImageField(blank=True, null=True, upload_to='service/logos/', verbose_name='logo'),
+ ),
+ ]
diff --git a/service/migrations/0008_service_available.py b/service/migrations/0008_service_available.py
new file mode 100644
index 0000000..f3c5efa
--- /dev/null
+++ b/service/migrations/0008_service_available.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2 on 2021-09-02 16:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('service', '0007_auto_20210902_2056'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='service',
+ name='available',
+ field=models.BooleanField(default=False, verbose_name='available'),
+ ),
+ ]
diff --git a/service/migrations/__init__.py b/service/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/service/models.py b/service/models.py
new file mode 100644
index 0000000..9d8da92
--- /dev/null
+++ b/service/models.py
@@ -0,0 +1,124 @@
+from django.db import models
+from django.utils.text import slugify
+
+from accounts.models import ServiceProvider
+from library.models import BaseModel
+from django.utils.translation import ugettext_lazy as _
+from address.models import ServiceAddress, Area
+import uuid
+
+
+class Service(BaseModel):
+ RESTAURANT = 0
+ CAFE = 1
+ CONFECTIONERY = 2
+ SUPERMARKET = 3
+
+ SERVICE_TYPES = (
+ (RESTAURANT, _('restaurant')),
+ (CAFE, _('cafe')),
+ (CONFECTIONERY, _('confectionery')),
+ (SUPERMARKET, _('supermarket')),
+ )
+
+ uuid = models.UUIDField(default=uuid.uuid4, verbose_name=_('uuid'), unique=True, db_index=True)
+ service_provider = models.ForeignKey(
+ ServiceProvider,
+ verbose_name=_('service provider'),
+ related_name='services',
+ on_delete=models.CASCADE
+ )
+ name = models.CharField(max_length=40, verbose_name=_('name'))
+ service_type = models.PositiveSmallIntegerField(verbose_name=_('service type'), choices=SERVICE_TYPES)
+ minimum_purchase = models.DecimalField(max_digits=9, decimal_places=0, verbose_name=_('minimum purchase'))
+ available = models.BooleanField(default=False, verbose_name=_('available'))
+ logo = models.ImageField(verbose_name=_('logo'), upload_to='service/logos/', null=True, blank=True)
+ banner = models.ImageField(verbose_name=_('banner'), upload_to='service/banners/', null=True, blank=True)
+
+ address = models.OneToOneField(
+ ServiceAddress,
+ verbose_name=_('address'),
+ related_name='services',
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True
+ )
+
+ def __str__(self):
+ return f'{self.name} - {self.get_service_type_display()}'
+
+ class Meta:
+ verbose_name = _('Service')
+ verbose_name_plural = _('Services')
+ db_table = 'service'
+ ordering = ('created_time',)
+
+
+class ServiceCategory(BaseModel):
+ name = models.CharField(max_length=30, verbose_name=_('name'))
+ slug = models.SlugField(max_length=35, verbose_name=_('slug'), allow_unicode=True)
+ service = models.ForeignKey(Service, verbose_name=_('service'), related_name='categories', on_delete=models.CASCADE)
+
+ def __str__(self):
+ return f'{self.name} - {self.service.name}'
+
+ def save(self, *args, **kwargs):
+ self.slug = slugify(self.name, allow_unicode=True)
+ super().save(*args, **kwargs)
+
+ class Meta:
+ verbose_name = _('ServiceCategory')
+ verbose_name_plural = _('ServiceCategories')
+ db_table = 'service_category'
+
+
+class DeliveryArea(BaseModel):
+ service = models.ForeignKey(
+ Service, verbose_name=_('service'), related_name='delivery_areas', on_delete=models.CASCADE
+ )
+ area = models.ForeignKey(Area, verbose_name=_('area'), related_name='delivery_areas', on_delete=models.CASCADE)
+
+ def __str__(self):
+ return f'{self.service.name} - {self.area}'
+
+ class Meta:
+ verbose_name = _('DeliveryArea')
+ verbose_name_plural = _('DeliveryAreas')
+ db_table = 'delivery_area'
+
+
+class ServiceAvailableTime(BaseModel):
+ SAT_DAY = 0
+ SUN_DAY = 1
+ MON_DAY = 2
+ TUE_DAY = 3
+ WED_DAY = 4
+ THU_DAY = 5
+ FRI_DAY = 6
+
+ DAYS = (
+ (SAT_DAY, _('saturday')),
+ (SUN_DAY, _('sunday')),
+ (MON_DAY, _('monday')),
+ (TUE_DAY, _('tuesday')),
+ (WED_DAY, _('wednesday')),
+ (THU_DAY, _('thursday')),
+ (FRI_DAY, _('friday'))
+ )
+ service = models.ForeignKey(
+ Service, verbose_name=_('service'), related_name='available_times', on_delete=models.CASCADE
+ )
+ day = models.PositiveSmallIntegerField(verbose_name=_('day'), choices=DAYS, null=True)
+ open_time = models.TimeField(verbose_name=_('open time'), null=True)
+ close_time = models.TimeField(verbose_name=_('close time'), null=True)
+ is_close = models.BooleanField(verbose_name=_('close day'), blank=True, null=True)
+
+ def __str__(self):
+ return f'{self.service.name} - {self.get_day_display()} - ' \
+ f'{self.is_close if self.close_time else self.open_time and "-" and self.close_time}'
+
+ class Meta:
+ verbose_name = _('ServiceAvailableTime')
+ verbose_name_plural = _('ServiceAvailableTimes')
+ db_table = 'service_available_time'
+ ordering = ('day',)
diff --git a/service/templates/delivery_area/create_update_form.html b/service/templates/delivery_area/create_update_form.html
new file mode 100644
index 0000000..3406e70
--- /dev/null
+++ b/service/templates/delivery_area/create_update_form.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% block content %}
+
+ {% if object %}
+ cancel
+ {% else %}
+ cancel
+ {% endif %}
+{% endblock %}
diff --git a/service/templates/delivery_area/delete_form.html b/service/templates/delivery_area/delete_form.html
new file mode 100644
index 0000000..37b3357
--- /dev/null
+++ b/service/templates/delivery_area/delete_form.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/service/templates/service/list.html b/service/templates/service/list.html
new file mode 100644
index 0000000..70013cd
--- /dev/null
+++ b/service/templates/service/list.html
@@ -0,0 +1,61 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load service_delivary_area %}
+{% block content %}
+
+
+
+
+ {% for service in services %}
+
+
+
+
+
+ {% if service.banner %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ {% if service.logo %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
{{ service.name.title }}
+
+
{{ service.address | truncatechars:30 }}
+ {% delivery_area_string service as areas %}
+
{{ areas | truncatechars:15 }}
+
{{ service.areas | truncatechars:20 }}
+
+
+
+
+ Menu
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/service/templates/service/service_provider/create_update_form.html b/service/templates/service/service_provider/create_update_form.html
new file mode 100644
index 0000000..4ab3de7
--- /dev/null
+++ b/service/templates/service/service_provider/create_update_form.html
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+{% block content %}
+
+ cancel
+{% endblock %}
diff --git a/service/templates/service/service_provider/delete_form.html b/service/templates/service/service_provider/delete_form.html
new file mode 100644
index 0000000..bd2fc5b
--- /dev/null
+++ b/service/templates/service/service_provider/delete_form.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/service/templates/service/service_provider/detail.html b/service/templates/service/service_provider/detail.html
new file mode 100644
index 0000000..d49d904
--- /dev/null
+++ b/service/templates/service/service_provider/detail.html
@@ -0,0 +1,119 @@
+{% extends 'base.html' %}
+{% block content %}
+
+
Service Detail
+
Service name : {{ service.name }}
+
Service minimum purchase : {{ service.minimum_purchase }}
+
Service minimum purchase : {{ service.get_service_type_display }}
+ {% if service.available %}
+
Service available : Yes
+ {% else %}
+
Service available : No
+ {% endif %}
+ {% if service.logo %}
+
Service Logo Image :
+ {% else %}
+
Service Logo Image : Not Yet Uploaded
+ {% endif %}
+ {% if service.banner %}
+
Service Banner Image :
+ {% else %}
+
Service Logo Image : Not Yet Uploaded
+ {% endif %}
+
+
+
+
+
Service Address
+ {% if service.address %}
+
state : {{ service.address.state.name }}
+
city : {{ service.address.city.name }}
+
area : {{ service.address.area.name }}
+
street : {{ service.address.street }}
+
alley : {{ service.address.alley }}
+
floor : {{ service.address.floor }}
+
plaque : {{ service.address.plaque }}
+
Update Address
+ {% else %}
+
Your service yet not has address!Create Address
+ {% endif %}
+
+
+
+
Service Categories
+
+
Create new category
+
+
+ {% for category in service.categories.all %}
+
+ {{ category.name }}
+
+
+ {% endfor %}
+
+
+
+
+
Service Delivery Area
+
+
+
Create new delivery area
+
+
+ {% with deliveries=service.delivery_areas.all %}
+ {% if deliveries %}
+ {% for delivery in deliveries %}
+
+ {{ delivery.area.name }}
+
+
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+
+
Service Available Time
+
+
+
Create new available
+ time
+
+
+ {% with availabletimes=service.available_times.all %}
+ {% if availabletimes %}
+ {% for availabletime in availabletimes %}
+
+ {{ availabletime.get_day_display }} | open at : {{ availabletime.open_time }} | close at
+ : {{ availabletime.close_time }} | is close : {{ availabletime.is_close }}
+
+
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+{% endblock %}
diff --git a/service/templates/service/service_provider/list.html b/service/templates/service/service_provider/list.html
new file mode 100644
index 0000000..e62ea8e
--- /dev/null
+++ b/service/templates/service/service_provider/list.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% block content %}
+ Create new service
+
+
+ {% for service in services %}
+
+ {{ service.name }}
+
+
+ {% endfor %}
+
+{% endblock %}
diff --git a/service/templates/service_available_time/create_update_form.html b/service/templates/service_available_time/create_update_form.html
new file mode 100644
index 0000000..3406e70
--- /dev/null
+++ b/service/templates/service_available_time/create_update_form.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% block content %}
+
+ {% if object %}
+ cancel
+ {% else %}
+ cancel
+ {% endif %}
+{% endblock %}
diff --git a/service/templates/service_available_time/delete_form.html b/service/templates/service_available_time/delete_form.html
new file mode 100644
index 0000000..d6e6b9e
--- /dev/null
+++ b/service/templates/service_available_time/delete_form.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/service/templates/service_category/create_update_form.html b/service/templates/service_category/create_update_form.html
new file mode 100644
index 0000000..6bcb436
--- /dev/null
+++ b/service/templates/service_category/create_update_form.html
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+{% block content %}
+
+ {% if service %}
+ cancel
+ {% else %}
+ cancel
+ {% endif %}
+{% endblock %}
diff --git a/service/templates/service_category/delete_form.html b/service/templates/service_category/delete_form.html
new file mode 100644
index 0000000..19ee37a
--- /dev/null
+++ b/service/templates/service_category/delete_form.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% block content %}
+
+{% endblock %}
diff --git a/service/templates/service_category/detail.html b/service/templates/service_category/detail.html
new file mode 100644
index 0000000..5800e01
--- /dev/null
+++ b/service/templates/service_category/detail.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% block content %}
+
+
Category Detail
+
Category name : {{ category.name }}
+
+
Service Items
+ {% if category.items.all %}
+
+ {% for item in category.items.all %}
+
+ {{ category.name }}
+
+
+ {% endfor %}
+
+ {% else %}
+
Your category yet not has item!Create Item
+ {% endif %}
+
+{% endblock %}
diff --git a/service/templatetags/__init__.py b/service/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/service/templatetags/service_delivary_area.py b/service/templatetags/service_delivary_area.py
new file mode 100644
index 0000000..0bd4aa1
--- /dev/null
+++ b/service/templatetags/service_delivary_area.py
@@ -0,0 +1,11 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag()
+def delivery_area_string(service):
+ area_list = list()
+ for delivery in service.delivery_areas.all():
+ area_list.append(delivery.area.name)
+ return ', '.join(area_list)
diff --git a/service/tests.py b/service/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/service/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/service/urls.py b/service/urls.py
new file mode 100644
index 0000000..df16dab
--- /dev/null
+++ b/service/urls.py
@@ -0,0 +1,48 @@
+from django.urls import path
+
+from .views import ServiceProviderServiceCreateView, ServiceProviderServiceUpdateView, ServiceProviderServiceDeleteView, \
+ ServiceProviderServiceDetailView, ServiceProviderServiceListView, ServiceProviderServiceCategoryCreateView, \
+ ServiceProviderServiceCategoryUpdateView, ServiceProviderServiceCategoryDeleteView, \
+ ServiceProviderDeliveryAreaCreate, ServiceProviderDeliveryUpdateView, \
+ ServiceProviderDeliveryDeleteView, ServiceProviderServiceAvailableTimeCreateView, \
+ ServiceProviderServiceAvailableTimeUpdateView, ServiceProviderServiceAvailableTimeDeleteView, ServiceListView
+
+app_name = 'service'
+
+urlpatterns = (
+ path('serviceprovider/create/', ServiceProviderServiceCreateView.as_view(),
+ name='service-provider-service-create'),
+ path('serviceprovider/update/', ServiceProviderServiceUpdateView.as_view(),
+ name='service-provider-service-update'),
+ path('serviceprovider/delete/', ServiceProviderServiceDeleteView.as_view(),
+ name='service-provider-service-delete'),
+ path('serviceprovider/detail/', ServiceProviderServiceDetailView.as_view(),
+ name='service-provider-service-detail'),
+ path('serviceprovider/list/', ServiceProviderServiceListView.as_view(), name='service-provider-service-list'),
+
+ path('serviceprovider/category/create//', ServiceProviderServiceCategoryCreateView.as_view(),
+ name='service-provider-service-category-create'),
+ path('serviceprovider/category/update//', ServiceProviderServiceCategoryUpdateView.as_view(),
+ name='service-provider-service-category-update'),
+ path('serviceprovider/category/delete//', ServiceProviderServiceCategoryDeleteView.as_view(),
+ name='service-provider-service-category-delete'),
+
+ path('serviceprovider/deliveryarea/create/', ServiceProviderDeliveryAreaCreate.as_view(),
+ name='service-provider-delivery-area-create'),
+ path('serviceprovider/deliveryarea/update/', ServiceProviderDeliveryUpdateView.as_view(),
+ name='service-provider-delivery-area-update'),
+ path('serviceprovider/deliveryarea/delete/', ServiceProviderDeliveryDeleteView.as_view(),
+ name='service-provider-delivery-area-delete'),
+
+ path(
+ 'serviceprovider/availabletime/create/',
+ ServiceProviderServiceAvailableTimeCreateView.as_view(),
+ name='service-provider-available-time-create'
+ ),
+ path('serviceprovider/availabletime/update/', ServiceProviderServiceAvailableTimeUpdateView.as_view(),
+ name='service-provider-available-time-update'),
+ path('serviceprovider/availabletime/delete/', ServiceProviderServiceAvailableTimeDeleteView.as_view(),
+ name='service-provider-available-time-delete'),
+
+ path('list/', ServiceListView.as_view(), name='service-list')
+)
diff --git a/service/utils.py b/service/utils.py
new file mode 100644
index 0000000..edc125b
--- /dev/null
+++ b/service/utils.py
@@ -0,0 +1,10 @@
+from abc import ABC
+
+from accounts.utils import IsServiceProvider
+
+
+class CustomServiceIsServiceProvider(ABC, IsServiceProvider):
+ def test_func(self):
+ result = super().test_func()
+ obj = self.get_object()
+ return obj.service_provider == self.request.user and result
diff --git a/service/views.py b/service/views.py
new file mode 100644
index 0000000..6671536
--- /dev/null
+++ b/service/views.py
@@ -0,0 +1,189 @@
+from abc import ABC
+
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.db import transaction
+from django.urls import reverse_lazy
+from django.utils.decorators import method_decorator
+from django.views.generic import CreateView, ListView, UpdateView, DeleteView, DetailView
+from django.shortcuts import get_object_or_404, redirect
+
+from django_filters.views import FilterView
+
+from accounts.utils import IsServiceProvider
+from service.forms import ServiceCreateUpdateForm, ServiceCategoryCreateUpdateForm, DeliveryAreaCreateUpdateForm, \
+ ServiceAvailableTimeCreateUpdateForm
+from service.filters import ServiceFilter
+from service.models import Service, ServiceCategory, DeliveryArea, ServiceAvailableTime
+from service.utils import CustomServiceIsServiceProvider
+
+
+class BaseServiceView(ABC):
+ model = Service
+ form_class = ServiceCreateUpdateForm
+ template_name = 'service/service_provider/create_update_form.html'
+
+
+class BaseCreateView(ABC, IsServiceProvider):
+ def test_func(self):
+ return self.service.service_provider == self.request.user and super().test_func()
+
+ def get_success_url(self):
+ return reverse_lazy('service:service-provider-service-detail', kwargs={'pk': self.service.pk})
+
+ def setup(self, request, *args, **kwargs):
+ super().setup(request, *args, **kwargs)
+ self.service = get_object_or_404(Service, pk=self.kwargs['service_pk'])
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['service'] = self.service
+ return context
+
+
+class BaseOtherView(IsServiceProvider):
+ def get_success_url(self):
+ return reverse_lazy('service:service-provider-service-detail', kwargs={'pk': self.object.service.pk})
+
+ def test_func(self):
+ obj = self.get_object()
+ return obj.service.service_provider == self.request.user and super().test_func()
+
+
+class BaseServiceCategory(ABC, BaseOtherView):
+ model = ServiceCategory
+ form_class = ServiceCategoryCreateUpdateForm
+ template_name = 'service_category/create_update_form.html'
+
+
+class BaseDeliveryArea(ABC, BaseOtherView):
+ model = DeliveryArea
+ form_class = DeliveryAreaCreateUpdateForm
+ template_name = 'delivery_area/create_update_form.html'
+
+
+class BaseServiceAvailableTime(ABC, BaseOtherView):
+ model = ServiceAvailableTime
+ form_class = ServiceAvailableTimeCreateUpdateForm
+ template_name = 'service_available_time/create_update_form.html'
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceCreateView(BaseServiceView, IsServiceProvider, CreateView):
+ success_url = reverse_lazy('service:service-provider-service-list')
+
+ def form_valid(self, form):
+ instance = form.save(commit=False)
+ instance.service_provider = self.request.user
+ instance.save()
+ return super().form_valid(form)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceUpdateView(BaseServiceView, CustomServiceIsServiceProvider, UpdateView):
+ success_url = reverse_lazy('service:service-provider-service-list')
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceDeleteView(BaseServiceView, CustomServiceIsServiceProvider, DeleteView):
+ template_name = 'service/service_provider/delete_form.html'
+ success_url = reverse_lazy('service:service-provider-service-list')
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceDetailView(BaseServiceView, CustomServiceIsServiceProvider, DetailView):
+ context_object_name = 'service'
+ template_name = 'service/service_provider/detail.html'
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceListView(BaseServiceView, IsServiceProvider, ListView):
+ context_object_name = 'services'
+ template_name = 'service/service_provider/list.html'
+
+ def get_queryset(self):
+ return super().get_queryset().filter(service_provider=self.request.user)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceCategoryCreateView(BaseCreateView, CreateView):
+ model = ServiceCategory
+ form_class = ServiceCategoryCreateUpdateForm
+ template_name = 'service_category/create_update_form.html'
+
+ def form_valid(self, form):
+ instance = form.save(commit=False)
+ instance.service = self.service
+ instance.save()
+ return super().form_valid(form)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceCategoryUpdateView(BaseServiceCategory, UpdateView):
+ pass
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceCategoryDeleteView(BaseServiceCategory, DeleteView):
+ context_object_name = 'category'
+ template_name = 'service_category/delete_form.html'
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderDeliveryAreaCreate(BaseCreateView, CreateView):
+ model = DeliveryArea
+ form_class = DeliveryAreaCreateUpdateForm
+ template_name = 'delivery_area/create_update_form.html'
+
+ def form_valid(self, form):
+ instance = form.save(commit=False)
+ instance.service = self.service
+ instance.save()
+ return super().form_valid(form)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderDeliveryUpdateView(BaseDeliveryArea, UpdateView):
+ pass
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderDeliveryDeleteView(BaseDeliveryArea, DeleteView):
+ context_object_name = 'delivery'
+ template_name = 'delivery_area/delete_form.html'
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceAvailableTimeCreateView(BaseCreateView, CreateView):
+ model = ServiceAvailableTime
+ form_class = ServiceAvailableTimeCreateUpdateForm
+ template_name = 'service_available_time/create_update_form.html'
+
+ def form_valid(self, form):
+ with transaction.atomic():
+ instance = form.save(commit=False)
+ if self.service.available_times.filter(day=instance.day).exists():
+ messages.info(self.request, f'This Service {self.service.name} has this day time', 'danger')
+ return redirect(self.get_success_url())
+ instance.service = self.service
+ instance.save()
+ return super().form_valid(form)
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceAvailableTimeUpdateView(BaseServiceAvailableTime, UpdateView):
+ pass
+
+
+@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch')
+class ServiceProviderServiceAvailableTimeDeleteView(BaseServiceAvailableTime, DeleteView):
+ context_object_name = 'available_time'
+ template_name = 'service_available_time/delete_form.html'
+
+
+class ServiceListView(FilterView):
+ model = Service
+ context_object_name = 'services'
+ template_name = 'service/list.html'
+ filterset_class = ServiceFilter
+ queryset = Service.objects.filter(available=True)
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..f2ad898
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,48 @@
+{% load accounts_tags %}
+
+
+
+
+
+
+
+
+
+ {% block title %} {% endblock %}
+ {% block extra_css %}{% endblock %}
+
+
+
+{% is_customer request.user as customer_flag %}
+{% is_service_provider request.user as service_provider_flag %}
+{% is_admin_user request.user as admin_flag %}
+
+
+{% if service_provider_flag %}
+ {% include 'inc/service_provider_navbar.html' %}
+{% elif customer_flag %}
+ {% include 'inc/customer_navbar.html' %}
+{% elif admin_flag %}
+ {% include 'inc/admin_navbar.html' %}
+{% else %}
+ {% include 'inc/customer_navbar.html' %}
+{% endif %}
+{% block banner %}{% endblock %}
+
+
+
+ {% if messages %}
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+ {% endif %}
+ {% block content %}
+ {% endblock %}
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/inc/admin_navbar.html b/templates/inc/admin_navbar.html
new file mode 100644
index 0000000..c27e362
--- /dev/null
+++ b/templates/inc/admin_navbar.html
@@ -0,0 +1,42 @@
+
+
+
\ No newline at end of file
diff --git a/templates/inc/customer_navbar.html b/templates/inc/customer_navbar.html
new file mode 100644
index 0000000..7532d8a
--- /dev/null
+++ b/templates/inc/customer_navbar.html
@@ -0,0 +1,35 @@
+
+
+
\ No newline at end of file
diff --git a/templates/inc/service_provider_navbar.html b/templates/inc/service_provider_navbar.html
new file mode 100644
index 0000000..9b0dc6c
--- /dev/null
+++ b/templates/inc/service_provider_navbar.html
@@ -0,0 +1,48 @@
+
+
+
\ No newline at end of file
diff --git a/yummy/__init__.py b/yummy/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/yummy/asgi.py b/yummy/asgi.py
new file mode 100644
index 0000000..8f47278
--- /dev/null
+++ b/yummy/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for yummy project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings')
+
+application = get_asgi_application()
diff --git a/yummy/settings.py b/yummy/settings.py
new file mode 100644
index 0000000..9623366
--- /dev/null
+++ b/yummy/settings.py
@@ -0,0 +1,141 @@
+"""
+Django settings for yummy project.
+
+Generated by 'django-admin startproject' using Django 3.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.2/ref/settings/
+"""
+
+import os
+from pathlib import Path
+from yummy.local_settings import *
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+
+ 'django_filters',
+
+ 'service.apps.ServiceConfig',
+ 'order.apps.OrderConfig',
+ 'address.apps.AddressConfig',
+ 'accounts.apps.AccountsConfig',
+ 'item.apps.ItemConfig',
+ 'cart.apps.CartConfig',
+ 'payment.apps.PaymentConfig',
+ 'gateway.apps.GatewayConfig',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'yummy.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [os.path.join(BASE_DIR, 'templates/')],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'yummy.wsgi.application'
+
+# Database
+# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'NAME': DB_NAME,
+ 'USER': DB_USER,
+ 'PASSWORD': DB_PASSWORD,
+ 'HOST': DB_HOST,
+ 'PORT': DB_PORT,
+ }
+}
+
+# Password validation
+# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'Asia/Tehran'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.2/howto/static-files/
+
+STATIC_URL = '/static/'
+STATIC_ROOT = os.path.join(BASE_DIR, 'static')
+
+MEDIA_URL = '/media/'
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+AUTHENTICATION_BACKENDS = [
+ 'django.contrib.auth.backends.ModelBackend',
+ 'accounts.authenticate.PhoneNumberPasswordBackend',
+ 'accounts.authenticate.ServiceProviderAuthentication',
+]
+
+CALL_BACK = 'http://127.0.0.1:8000/payment/verify/'
diff --git a/yummy/urls.py b/yummy/urls.py
new file mode 100644
index 0000000..24fddd1
--- /dev/null
+++ b/yummy/urls.py
@@ -0,0 +1,37 @@
+"""yummy URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.2/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.conf import settings
+from django.conf.urls.static import static
+from django.contrib import admin
+from django.urls import path, include
+from django.views.generic import TemplateView
+
+urlpatterns = [
+ path('admin/', admin.site.urls, name='admin-panel'),
+ path('home/', TemplateView.as_view(template_name='base.html'), name='home'),
+ path('', TemplateView.as_view(template_name='base.html'), name='home'),
+ path('accounts/', include('accounts.urls', namespace='accounts')),
+ path('service/', include('service.urls', namespace='service')),
+ path('address/', include('address.urls', namespace='address')),
+ path('item/', include('item.urls', namespace='item')),
+ path('cart/', include('cart.urls', namespace='cart')),
+ path('payment/', include('payment.urls', namespace='payment')),
+ path('order/', include('order.urls', namespace='order')),
+
+]
+
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/yummy/wsgi.py b/yummy/wsgi.py
new file mode 100644
index 0000000..c07aee4
--- /dev/null
+++ b/yummy/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for yummy project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings')
+
+application = get_wsgi_application()