diff --git a/cart/admin.py b/cart/admin.py index 8c38f3f..1b23730 100644 --- a/cart/admin.py +++ b/cart/admin.py @@ -1,3 +1,143 @@ +import csv +import datetime +import pytils +import weasyprint +from decimal import Decimal + +from django.conf import settings from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ +from django.http import HttpResponse +from django.template.loader import render_to_string + +from jet.admin import CompactInline +from jet.filters import DateRangeFilter +from rangefilter.filter import DateTimeRangeFilter + +from core.admin import SafeModelAdmin +from core.models import Certificate +from eshop_project.settings.base import PAY_REQUISITES +from .models import ( + Offer, SupplyType, + Currency, Buying, + SupplyTarget, + Order, Discount, + Client) + + +class ProductOfferInlineAdmin(CompactInline): + model = Offer + exclude = ('status',) + extra = 1 + show_change_link = 1 + max_num = 1 + +# Supply admins + +@admin.register(SupplyType) +class SupplyTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'min_term', 'max_term') + search_fields = ('name', 'min_term', 'max_term') + + +@admin.register(SupplyTarget) +class SupplyTargetAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name', 'slug',) + + +@admin.register(Discount) +class DiscountAdmin(admin.ModelAdmin): + list_display = ['code', 'valid_from', 'valid_to', 'value', 'active'] + list_filter = ['valid_from', 'valid_to', 'active'] + search_field = ['code'] + + +# Offer admins +@admin.register(Offer) +class ProductOfferAdmin(SafeModelAdmin): + list_display = ('product', 'price', 'amount', 'currency') + search_fields = ('product__name',) + list_filter = ('currency',) + + +@admin.register(Buying) +class BuyingAdmin(SafeModelAdmin): + def export_buyings_to_csv(self, buying, queryset): + opts = buying._meta + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename=Orders-{}.csv'.format( + datetime.datetime.now().strftime("%d/%m/%Y")) + writer = csv.writer(response) + + fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many] + + writer.writerow([field.verbose_name for field in fields]) + + for obj in queryset: + data_row = [] + for field in fields: + value = getattr(obj, field.name) + if isinstance(value, datetime.datetime): + value = value.strftime('%d/%m/%Y') + + data_row.append(value) + writer.writerow(data_row) + return response + export_buyings_to_csv.short_description = _('экспортировать CSV') + + def print_order_in_pdf(self,buyings): + verb_price = pytils.numeral.in_words(round(buyings.total_price)) + verb_cur = pytils.numeral.choose_plural(round(buyings.total_price), ("рубль", "рубля", "рублей")) + html = render_to_string('bootstrap/pdf/buyings.html', { + **PAY_REQUISITES, 'order': buyings, 'verb_cur': verb_cur, 'verb_price': verb_price + }) + rendered_html = html.encode(encoding="UTF-8") + response = HttpResponse(content_type='application/pdf') + response['Content-Disposition'] = 'filename=order_{}.pdf'.format(buyings.id) + + + weasyprint.HTML( + string=rendered_html, + base_url=self.request.build_absolute_uri() + ).write_pdf( + response, + stylesheets = [ + weasyprint.CSS(settings.STATIC_ROOT + '/css/bootstrap.min.css') + ] + ) + return response + print_order_in_pdf.short_description = _('Распечатать заказ в pdf') + + def mark_buyings_as_paid(self, request, queryset): + for buying in queryset: + user_profile = buying.user.profile + if user_profile.parent: + parent_profile = user_profile.parent.profile + parent_profile.user_points += round(buying.total_price * Decimal(0.01)) + parent_profile.save() + buying.status = BUYING_STATUS_PAID + buying.save() + mark_buyings_as_paid.short_description = _('Отметить как оплаченные') + + + inlines = () + list_display = ('user', 'offer', 'status', 'amount', 'total_price') + search_fields = ('user', 'offer',) + list_filter = ( + ('create_at', DateRangeFilter), ('updated_at', DateTimeRangeFilter) + ) + + actions = (export_buyings_to_csv, print_order_in_pdf, mark_buyings_as_paid) + + +@admin.register(Order) +class OrderAdmin(SafeModelAdmin): + list_display = ('order_code', 'customer_user', 'customer_name', 'customer_email','phone') + -# Register your models here. +@admin.register(Client) +class ClientAdmin(SafeModelAdmin): + list_display = ('name','image','status',) + search_fields = ('name',) + list_filter = ('status',) diff --git a/cart/cart.py b/cart/cart.py deleted file mode 100644 index bfb83fe..0000000 --- a/cart/cart.py +++ /dev/null @@ -1,87 +0,0 @@ -from decimal import Decimal -from django.conf import settings -from django.contrib import auth -from products.models import Product -# from discount.models import Discount - -class Cart(object): - def __init__(self, request): - self.session = request.session - # self.discount_id = self.session.get('discount_id') - if request.user.is_authenticated(): - # self.points = self.session.get('points') - self.points_quant = auth.get_user(request).profile.user_points - cart = self.session.get(settings.CART_SESSION_ID) - if not cart: - request.session['points'] = False - cart = self.session[settings.CART_SESSION_ID] = {} - self.cart = cart - - def add(self, offer, price_per_itom, quantity=1, update_quantity=False): - offer_slug = offer.slug - if offer_slug not in self.cart: - self.cart[offer_slug] = {'quantity': 0, - 'price': str(price_per_itom)} - if update_quantity: - self.cart[offer_slug]['quantity'] = int(quantity) - else: - self.cart[offer_slug]['quantity'] += int(quantity) - self.save() - - def save(self): - self.session[settings.CART_SESSION_ID] = self.cart - self.session.modified = True - - def remove(self, offer_slug): - # product_id = str(products.id) - if offer_slug in self.cart: - del self.cart[offer_slug] - self.save() - - def __iter__(self): - offers_ids = self.cart.keys() - offers = Offer.objects.filter(slug__in=offers_ids) - - for offer in offers: - self.cart[str(offer.slug)]['offer'] = offer - - for item in self.cart.values(): - item['price'] = Decimal(item['price']) - item['total_price'] = item['price'] * item['quantity'] - yield item - - def __len__(self): - return sum(item['quantity'] for item in self.cart.values()) - - def get_total_price(self): - return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values()) - - def get_max(self): - return min(self.points_quant, self.get_total_price() - 1) - - def clear(self): - del self.session[settings.CART_SESSION_ID] - self.session.modified = True - - # @property - # def discount(self): - # if self.discount_id: - # return Discount.objects.get(id=self.discount_id) - # return None - - # def get_discount(self): - # if self.discount: - # return (self.discount.discount / Decimal('100')) * self.get_total_price() - # return Decimal('0') - - # def get_total_price_after_discount(self): - # return self.get_total_price() - self.get_discount() - - def get_total_deduct_points(self): - total_price = self.get_total_price() - if total_price <= self.points_quant: - # self.points_quant = self.points_quant - total_price + 1 - # self.save() - return 1 - return total_price - self.points_quant - diff --git a/cart/context_processors.py b/cart/context_processors.py index c6dba27..a0a493e 100644 --- a/cart/context_processors.py +++ b/cart/context_processors.py @@ -1,6 +1,6 @@ -from .cart import Cart +from cart.utils import Cart -def cart(request): - return {'cart': Cart(request)} +def cart_basket(request): + return {'cart': Cart(request) } diff --git a/cart/fixtures/supply_targets.json b/cart/fixtures/supply_targets.json new file mode 100644 index 0000000..a9a38aa --- /dev/null +++ b/cart/fixtures/supply_targets.json @@ -0,0 +1 @@ +[{"model": "cart.supplytarget", "pk": 1, "fields": {"create_at": "2018-08-12T20:48:30.165Z", "updated_at": "2018-08-12T20:48:30.165Z", "name": "\u0414\u043e\u043c\u0430\u0448\u043d\u044f\u044f", "slug": "domashnyaya", "status": 25}}, {"model": "cart.supplytarget", "pk": 2, "fields": {"create_at": "2018-08-12T20:48:38.954Z", "updated_at": "2018-08-12T20:49:01.074Z", "name": "\u0413\u043e\u0441\u0443\u0434\u0430\u0440\u0441\u0442\u0432\u0435\u043d\u043d\u0430\u044f", "slug": "gosudarstvennaya", "status": 75}}, {"model": "cart.supplytarget", "pk": 3, "fields": {"create_at": "2018-08-12T20:48:49.758Z", "updated_at": "2018-08-12T20:48:49.758Z", "name": "\u041a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u0430\u044f", "slug": "korporativnaya", "status": 50}}, {"model": "cart.supplytarget", "pk": 4, "fields": {"create_at": "2018-08-12T20:49:11.950Z", "updated_at": "2018-08-12T20:49:11.950Z", "name": "\u0410\u043a\u0430\u0434\u0435\u043c\u0438\u0447\u0435\u0441\u043a\u0430\u044f", "slug": "akademicheskaya", "status": 100}}] \ No newline at end of file diff --git a/cart/fixtures/supply_types.json b/cart/fixtures/supply_types.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/cart/fixtures/supply_types.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/cart/forms.py b/cart/forms.py index f510932..b0f649f 100644 --- a/cart/forms.py +++ b/cart/forms.py @@ -1,21 +1,257 @@ +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Field, Div, HTML, Hidden, Fieldset, Submit from django import forms +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator +from django.forms import ALL_FIELDS, formset_factory +from django.urls import reverse_lazy -class CartAddProductForm(forms.Form): - quantity = forms.CharField(widget=forms.TextInput(attrs={ - 'id': 'quantity', - 'name': 'quantity', - 'type': 'number', - 'min': '1', - 'max': '1000', - 'value': '1', - 'onchange': 'calculate()'})) - product_slug = forms.CharField(widget=forms.TextInput(attrs={ - 'id': 'product_slug', - 'name': 'product_slug', - 'type': 'hidden'})) - price_per_itom = forms.IntegerField(widget=forms.TextInput(attrs={ - 'id': 'price_per_itom', - 'name': 'price_per_itom', - 'type': 'hidden'})) - update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput) +from cart.models import ( + Buying, BUYING_STATUS_IN_CART, Offer, SupplyType, SupplyTarget, Discount, Order +) +from core.forms import QueryFormBase +from core.utils import parse_path +from django.utils.translation import ugettext_lazy as _ +from products.models import Product + + +class CartAddInlineForm(forms.ModelForm): + form_action = {'viewname': 'cart:add', 'kwargs': {}} + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_method = 'post' + self.helper.form_action = reverse_lazy(**self.form_action) + self.helper.layout = Layout( + Field('offer'), + Field('amount'), + Div( + Submit('Купить',value='submit'), + css_class='catalog__btn' + ) + ) + + super().__init__(*args, **kwargs) + + + + def clean_amount(self): + amount = self.cleaned_data['amount'] + offer = self.cleaned_data['offer'] + if amount > offer.amount: + raise ValidationError('Колличество товара указано больше доступного') + elif amount <= 0: + raise ValidationError('Укажите колличество товара больше 0') + return amount + + def save(self, cart, user, commit=True): + offer = Offer.active.get(self.offer) + self.instance.user = user + self.instance.offer = offer + self.instance.amount = self.cart[offer.product.id]['quantity'] + self.instance.total_price = offer.product * self.cart[offer.product.id]['quantity'] + return super().save(commit) + + class Meta: + model = Buying + fields = ('offer', 'amount',) + widgets = { + 'offer': forms.HiddenInput(), + 'amount': forms.HiddenInput() + } + + +class CartRemoveBuyingForm(forms.ModelForm): + form_action = {'viewname': 'cart:remove', 'kwargs': {}} + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_method = 'post' + self.helper.form_action = reverse_lazy(**self.form_action) + self.helper.layout = Layout( + Field('offer'), + Div( + Submit('Убрать'), + css_class='catalog__btn' + ) + ) + + super().__init__(*args, **kwargs) + + class Meta: + model = Buying + fields = ('offer',) + widgets = { + 'offer': forms.HiddenInput() + } + + +CartRemoveBuyingFormset = formset_factory(CartRemoveBuyingForm) + + +class CartCheckoutForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_method = 'post' + self.helper.form_action = reverse_lazy(**self.form_action) + self.helper.layout = Layout( + Field('customer_name'), + Field('customer_email'), + Field('customer_user'), + Field('phone'), + Field('customer_address'), + Field('city'), + Field('buyings'), + Field('comment'), + Div( + Submit('Подвердить'), + css_class='catalog__btn' + ) + ) + + super().__init__(*args, **kwargs) + + class Model: + model = Order + fields = ( + 'customer_name', 'customer_email', 'customer_user', + 'phone', 'customer_address', 'city', 'buyings', + 'comment' + ) + +class ProductOfferPriceFilterForm(QueryFormBase): + min_price = 0 + max_price = 9999 + price = forms.IntegerField(min_value=0, max_value=0) + field_template = 'bootstrap/forms/product_filter.html' + title = _('Цена') + + def __init__(self, *args, **kwargs): + + self.helper = FormHelper() + self.helper.form_method = 'get' + self.helper.layout = Layout( + Div(HTML(self.title), css_class='category__title left-menu__price-item'), + Field('price', template=self.field_template) + ) + + super().__init__(*args, **kwargs) + + self.helper.form_action = reverse_lazy(**self.form_action) + + self.init_price_bounders() + self.init_field_params() + + def init_price_bounders(self): + if Offer.active.exists(): + off_qs = Offer.active + category_instance = '' + if self.form_action.get('kwargs', None): + category_instance = parse_path(self.form_action.get('kwargs').get('path', '')) + + off_qs = Offer.active.filter(product__parent__name=category_instance, + product__name__icontains=self.query_params.get('name', '')) + if off_qs.exists(): + self.min_price = round(off_qs.order_by('price').only('price').first().price, 0) + self.max_price = round(off_qs.order_by('-price').only('price').first().price, 0) + + def init_field_params(self): + for field in self.fields: + if field == 'price': + self.fields[field].validators = [ + MaxValueValidator(self.max_price), + MinValueValidator(self.min_price) + ] + + def get_initial_for_field(self, field, field_name): + return super().get_initial_for_field(field, field_name) + + +class ProductOfferSupplyTypeFilterForm(QueryFormBase): + supply_type = forms.ChoiceField() + + field_template = 'bootstrap/forms/product_filter.html' + title = _('Тип поставки') + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_method = 'get' + self.helper.layout = Layout( + Div(HTML(self.title), css_class='category__title'), + Field('supply_type', template=self.field_template) + ) + super().__init__(*args, **kwargs) + + self.helper.form_action = reverse_lazy(**self.form_action) + + def get_initial_for_field(self, field, field_name): + if field_name == 'supply_type': + sup_typ_qs = SupplyType.objects + category_instance = '' + if self.form_action.get('kwargs', None): + category_instance = parse_path(self.form_action.get('kwargs').get('path', '')) + + off_qs = Offer.active.filter(product__parent__name=category_instance, + product__name__icontains=self.query_params.get('name', '')) + if off_qs.count(): + sup_typ_qs = sup_typ_qs.filter(offer__pk__in=off_qs.all()) + + return sup_typ_qs.distinct('name').only('name', 'slug') + return super().get_initial_for_field(field, field_name) + + +class ProductOfferSupplyTargetFilterForm(QueryFormBase): + supply_target = forms.ChoiceField() + + field_template = 'bootstrap/forms/product_filter.html' + title = _('Назначение') + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_method = 'get' + self.helper.layout = Layout( + Div(HTML(self.title), css_class='category__title'), + Field('supply_target', template=self.field_template) + ) + super().__init__(*args, **kwargs) + + self.helper.form_action = reverse_lazy(**self.form_action) + + def get_initial_for_field(self, field, field_name): + if field_name == 'supply_target': + sup_tar_qs = SupplyTarget.objects + category_instance = '' + if self.form_action.get('kwargs', None): + category_instance = parse_path(self.form_action.get('kwargs').get('path', '')) + + off_qs = Offer.active.filter(product__parent__name=category_instance, + product__name__icontains=self.query_params.get('name', '')) + + if off_qs.count(): + sup_tar_qs = sup_tar_qs.filter(product__pk__in=off_qs.all()) + + return sup_tar_qs.distinct('name').only('name', 'slug') + return super().get_initial_for_field(field, field_name) + + +# @TODO: NOT IMPLEMENTED ON THE FRONT END. TEST BEFORE PRODUCTION +class DiscountForm(forms.ModelForm): + class Meta: + model = Discount + fields = ('code',) + + +class OrderCreateForm(forms.ModelForm): + customer_name = forms.CharField(max_length=100, required=True, label='Customer_name', + widget=forms.TextInput(attrs={'placeholder': 'Ф.И.О.'})) + customer_phone = forms.CharField(required=True, label='Customer_phone', + widget=forms.TextInput(attrs={'placeholder': 'номер телефона'})) + customer_email = forms.EmailField(required=True, label='Customer_email', + widget=forms.TextInput(attrs={'placeholder': 'e-mail'})) + city = forms.CharField(max_length=100, label='City', widget=forms.TextInput(attrs={'placeholder': 'город'})) + + class Meta: + model = Order + exclude = ('status',) diff --git a/cart/middleware.py b/cart/middleware.py new file mode 100644 index 0000000..b30fcd3 --- /dev/null +++ b/cart/middleware.py @@ -0,0 +1,13 @@ +from cart.utils import Cart + + +class CartMonkeyPatchingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + request.cart = Cart(request) + + response = self.get_response(request) + + return response diff --git a/cart/models.py b/cart/models.py index 988f98e..22a3b3f 100644 --- a/cart/models.py +++ b/cart/models.py @@ -1,45 +1,170 @@ +from django.utils.timezone import now, timedelta +import uuid + +from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator + from django.db import models from django.contrib.auth import get_user_model -from django.contrib.postgres.fields import HStoreField from django.db.models import Avg +from django.db.models.signals import post_save +from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from autoslug import AutoSlugField # Create your models here. -from core.models import AbstractStatusModel, AbstractDateTimeModel +from core.models import ( + AbstractStatusModel, AbstractDateTimeModel, + ActiveOnlyManager, + City, Currency) + from products.models import Product + +# -----------------------------------------Client ------------------------------------------------------# + +class Client(AbstractStatusModel): + def upload_file_to(self, filename): + return "clients/{name}/{filename}".format(**{ + 'name': self.name, + 'filename': filename + }) + + name = models.CharField(_('Название'), max_length=255) + image = models.FileField(_('Изображение'), upload_to=upload_file_to) + preview = models.FileField(_('Миниатюрка'), upload_to=upload_file_to) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Клиент') + verbose_name_plural = _('Клиенты') + + +# -------------------------------------- Supply type dimensions --------------------------------------# + +SUPPLY_TYPE_HOUR_DIMENSION = 0 +SUPPLY_TYPE_DAY_DIMENSION = 0 +SUPPLY_TYPE_WEEK_DIMENSION = 0 +SUPPLY_TYPE_MONTH_DIMENSION = 0 + +SUPPLY_TYPE_DIMENSION_CHOICES = ( + (SUPPLY_TYPE_HOUR_DIMENSION, _('Час')), + (SUPPLY_TYPE_DAY_DIMENSION, _('День')), + (SUPPLY_TYPE_WEEK_DIMENSION, _('Неделя')), + (SUPPLY_TYPE_MONTH_DIMENSION, _('Месяц')) +) + +SUPPLY_TYPE_DEFAULT_DIMENSION = SUPPLY_TYPE_HOUR_DIMENSION + + +class SupplyType(AbstractDateTimeModel): + name = models.CharField(_('Тип'), max_length=255) + slug = AutoSlugField(populate_from='name', unique=True) + min_term = models.IntegerField(_('от'), help_text=_('Минимальный срок поставки')) + max_term = models.IntegerField(_('до'), help_text=_('Максимальный срок поставки')) + term_dimension = models.SmallIntegerField( + _('размерность'), + choices=SUPPLY_TYPE_DIMENSION_CHOICES, + default=SUPPLY_TYPE_DEFAULT_DIMENSION + ) + + def get_formatted_desc(self): + return _(" от {from} до {to} {dimension}".format(**{ + 'from': self.min_term, + 'to': self.max_term, + 'dimension': SUPPLY_TYPE_DIMENSION_CHOICES[self.term_dimension][1] + })) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Тип поставки') + verbose_name_plural = _('Тип поставки') + + +class SupplyTarget(AbstractDateTimeModel): + name = models.CharField(_('Назначение'), max_length=255) + slug = AutoSlugField(populate_from='name', unique=True) + status = models.PositiveSmallIntegerField(_('статус'), help_text=_('Необходимо указать числовой код статус')) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('Лицензия') + verbose_name_plural = _('Лицензии') + + +class Discount(AbstractDateTimeModel): + code = models.CharField(max_length=50, blank=True, unique=True, default=str(uuid.uuid4())) + valid_from = models.DateTimeField(auto_now_add=True, blank=True) + valid_to = models.DateTimeField(default=now() + timedelta(days=7), blank=True) + value = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(100)], default=0, + help_text=_('Указываем целым числом. Пример: 30 = 30%')) + active = models.BooleanField(default=True) + + def __str__(self): + return self.code + + class Meta: + verbose_name = _('Дисконт') + verbose_name_plural = _('Дисконт') + + +# -------------------------------------- Offer status list -------------------------------------_# + +OFFER_STATUS_INACTIVE = 0 OFFER_STATUS_ACTIVE = 25 -OFFER_STATUS_INACTIVE = 50 +OFFER_STATUS_DELETED = 50 OFFER_STATUS_CHOICES = ( (OFFER_STATUS_ACTIVE, _('Активный')), - (OFFER_STATUS_INACTIVE, _('Неактивный')) + (OFFER_STATUS_INACTIVE, _('Неактивный')), + (OFFER_STATUS_DELETED, _('Удаленный')) ) OFFER_DEFAULT_CHOICE = OFFER_STATUS_INACTIVE -class SupplyType(AbstractDateTimeModel): - name = models.CharField(max_length=255) - term = models.ValueRange() +class Offer(AbstractStatusModel): + product = models.OneToOneField(Product, on_delete=models.CASCADE, primary_key=True, verbose_name=_('Продукт')) + vendor_code = models.CharField(_('Артикул'), max_length=255, unique=True, help_text=_('Должен быть уникальным')) -class Offer(AbstractStatusModel): - product = models.OneToOneField(Product, on_delete=models.CASCADE, primary_key=True) - price = models.DecimalField(max_digits=8, decimal_places=2, null=True, default=0.00) - currency = models.CharField(max_length=64, blank=True, null=True, default=None) - with_nds = models.BooleanField(default=False) - # attributes = HStoreField(blank=True, null=True, default={}) - suply_type = models.ForeignKey(SupplyType, on_delete=models.SET_NULL, blank=True, null=True) - note = models.TextField(blank=True, null=True) - status = models.SmallIntegerField(_('статус'), default=OFFER_DEFAULT_CHOICE, choices=OFFER_STATUS_CHOICES) + price = models.DecimalField(_('цена'), max_digits=10, decimal_places=2, validators=[MinValueValidator(1.00)], + help_text=_('Цена за продукт')) + currency = models.ForeignKey(Currency, verbose_name=_('Валюта'), on_delete=models.PROTECT, + help_text=_('Цена по умолчанию в рублях')) + supply_type = models.ForeignKey(SupplyType, verbose_name=_('Поставка'), on_delete=models.SET_NULL, blank=True, + null=True) + + supply_target = models.ForeignKey(SupplyTarget, on_delete=models.SET_NULL, verbose_name=_('Лицензия'), blank=True, + null=True, default=None) + + discount = models.ForeignKey(Discount, on_delete=models.SET_NULL, verbose_name=_('Дисконт'), blank=True, null=True) + + amount = models.IntegerField(_('Колличество'), default=1, validators=[MinValueValidator(1)]) + + cashback = models.DecimalField(_('Кешбек'), max_digits=6, decimal_places=2, default=0, + help_text=_('Указаная сумма будет отображаться в выбранной валюте позиции')) + + note = models.TextField(_('Пометка'), blank=True, null=True) - amount = models.IntegerField(blank=True, null=True) + account_nds = models.BooleanField(_('с учетом НДС'), default=False) + + def get_price_with_discount(self): + return self.price - ((self.discount.value * self.price) if self.discount else 0) def __str__(self): - return self.name + return self.product.name + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if self.product: + self.status = self.product.status + super().save(force_insert, force_update, using, update_fields) class Meta: verbose_name = _('Позиция') @@ -49,21 +174,62 @@ class Offer(AbstractStatusModel): # ------------------------------------------ Buying status --------------------------------------------------- # BUYING_STATUS_IN_CART = 25 BUYING_STATUS_PENDING = 50 -BUYING_STATUS_BOUGHT = 75 +BUYING_STATUS_PAID = 75 BUYING_STATUS_CHOICES = ( (BUYING_STATUS_IN_CART, _('В корзине')), (BUYING_STATUS_PENDING, _('Обрабатываеться')), - (BUYING_STATUS_BOUGHT, _('Куплен')) + (BUYING_STATUS_PAID, _('Оплаченно')) ) BUYING_DEFAULT_CHOICE = BUYING_STATUS_IN_CART +class BuyingManager(ActiveOnlyManager, models.Manager): + def get_user_buyings(self, user): + qs = self.get_queryset() + return qs.filter(user=user).all() + + def get_buying_total_price(self, user=None): + qs = self.get_user_buyings(user) if user else self.get_queryset() + return qs.aggregate(Sum('total_price')) + + def get_buying_total_bonus_points(self, user=None): + qs = self.get_user_buyings(user) if user else self.get_queryset() + return qs.aggregate(Sum('bonus_points')) + + def get_buying_total_cashback(self, user=None): + qs = self.get_user_buyings(user) if user else self.get_queryset() + return qs.select_related('buying_cashback').aggregate(Sum('amount')) + + class Buying(AbstractStatusModel): user = models.ForeignKey(get_user_model(), verbose_name=_('пользователь'), on_delete=models.CASCADE) offer = models.ForeignKey(Offer, verbose_name=_('позиция'), on_delete=models.CASCADE) + bonus_points = models.IntegerField(_('бонусы'), validators=(MinValueValidator(0),)) status = models.SmallIntegerField(_('статус'), default=BUYING_DEFAULT_CHOICE, choices=BUYING_STATUS_CHOICES) amount = models.SmallIntegerField(_('колличество'), default=0) + total_price = models.DecimalField(_('цена'), max_digits=10, decimal_places=2) + + active = BuyingManager() + + @property + def is_in_cart(self): + return self.status == BUYING_STATUS_IN_CART + + @property + def is_pending(self): + return self.status == BUYING_STATUS_PENDING + + @property + def is_paid(self): + return self.status == BUYING_STATUS_PAID + + def __str__(self): + return "{product_name}({product_amount}) - {price}".format(**{ + 'product_name': self.offer.product.name, + 'product_amount': self.amount, + 'price': self.total_price + }) class Meta: verbose_name = _('Покупка') @@ -80,7 +246,7 @@ CASHBACK_STATUS_CHOICES = ( STATUS_DEFAULT = STATUS_GAINED -class CashBackManager(models.Manager): +class CashBackManager(ActiveOnlyManager): def get_gained_cashback_sum(self, user): return self.get_queryset().filter(user=user, status=STATUS_GAINED).aggregate(Avg('amount')) @@ -88,16 +254,78 @@ class CashBackManager(models.Manager): return self.get_queryset().filter(user=user, status=STATUS_SPENT).aggregate(Avg('amount')) -class BuyingCashback(AbstractDateTimeModel): +class Cashback(AbstractDateTimeModel): user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - buying = models.OneToOneField(Buying, on_delete=models.CASCADE) amount = models.DecimalField(_('Сумма'), 'cashback', 7, 2, default=0) status = models.SmallIntegerField(_('статус'), default=STATUS_DEFAULT, choices=CASHBACK_STATUS_CHOICES) objects = CashBackManager() + @property + def is_spent(self): + return self.status == STATUS_SPENT + class Meta: verbose_name_plural = _('cashback') verbose_name = _('cashback') +ORDER_STATUS_NEW = 0 +ORDER_STATUS_PENDING = 50 +ORDER_STATUS_PAID = 100 + +ORDER_STATUS_CHOICES = ( + (ORDER_STATUS_NEW, _('Новый')), + (ORDER_STATUS_PENDING, _('Обрабатывается')), + (ORDER_STATUS_PAID, _('Оплаченно')) +) + +ORDER_STATUS_DEFAULT = ORDER_STATUS_NEW + + +class Order(AbstractStatusModel): + order_code = models.CharField(_('код заказа'), max_length=255, default=str(uuid.uuid4())) + customer_name = models.CharField(_('bмя'), max_length=255) + customer_email = models.EmailField(_('email'), blank=True, null=True, default=None) + customer_user = models.ForeignKey( + get_user_model(), on_delete=models.SET_NULL, + verbose_name=_('пользователь'), + blank=True, null=True + ) + + phone_regex = RegexValidator( + regex=r'^\((+7)|8)?\d{10}$', + message="Phone number must be entered in the format: '+99999999999'. Up to 12 digits allowed." + ) + phone = models.CharField(_('телефон'), validators=[phone_regex], max_length=12) + + customer_address = models.TextField(_('адрес')) + city = models.ForeignKey(City, on_delete=models.PROTECT, verbose_name=_('Город')) + + buyings = models.ManyToManyField(Buying, verbose_name=_('Покупки')) + total_price = models.DecimalField(_('стоимость'), max_digits=10, decimal_places=2, default=0) + comment = models.TextField(_('комментарий'), blank=True, null=True, default=None) + status = models.SmallIntegerField(_('статус'), default=ORDER_STATUS_CHOICES, choices=ORDER_STATUS_DEFAULT) + + def __str__(self): + return self.order_code + + class Meta: + ordering = ('-create_at',) + verbose_name = _('Заказ') + verbose_name_plural = _('Заказы') + + +@receiver(post_save, sender=Order) +def product_in_order_post_save(sender, instance, created, **kwargs): + order = instance.order + all_products_in_order = Buying.objects.filter(order=instance, status=ORDER_STATUS_NEW) + + order_total_price = sum(item.total_price for item in all_products_in_order) + # if order.discount: + # order.total_price = order_total_price * (order.discount_value / Decimal('100')) + if order.points_quant: + order.total_price = order_total_price - order.points_quant + else: + order.total_price = order_total_price + order.save(force_update=True) diff --git a/cart/tasks.py b/cart/tasks.py new file mode 100644 index 0000000..6693d0b --- /dev/null +++ b/cart/tasks.py @@ -0,0 +1,45 @@ +import celery +from django.conf import settings +from celery import task +from django.template.loader import render_to_string +from django.core.mail import send_mail, EmailMessage +from io import BytesIO +import weasyprint +import pytils + +from cart.models import Order + +SUPPLIER_INFO = '''ООО "Русские Программы", ИНН 7713409230, КПП 771301001, + 127411, Москва г, Дмитровское ш., дом № 157, корпус 7, тел.: +74957258950''' + +requisites = {'name': 'ООО "Русские Программы"', 'bank': 'АО "СМП БАНК" Г. МОСКВА', 'INN': '7713409230', + 'KPP': '771301001', 'BIK': '44525503', 'bank_acc': '30101810545250000503', 'acc': '40702810300750000177', + 'sup_info': SUPPLIER_INFO} + +@celery.task +def send_user_order_notification(order_id): + """ + Sending Email of order creating + """ + order = Order.objects.get(id=order_id) + verb_price = pytils.numeral.in_words(round(order.total_price)) + verb_cur = pytils.numeral.choose_plural(round(order.total_price), ("рубль", "рубля", "рублей")) + subject = 'Заказ № {}'.format(order.id) + message = 'Уважаемый, {}, номер Вашего заказа {}. \ + Пожалуйста, совершите платеж по поручению в приложении к этому письму в течение 14 дней.'.format(order.customer_name, order.id) + mail_send = EmailMessage(subject, message, 'admin@myshop.ru', [order.customer_email]) + + # html = render_to_string('orders:AdminOrderPDF', args=[order_id]) + + html = render_to_string('orders/pdf.html', {**requisites, 'order': order, + 'verb_cur': verb_cur, 'verb_price': verb_price}) + rendered_html = html.encode(encoding="UTF-8") + out = BytesIO() + weasyprint.HTML(string=rendered_html).write_pdf(out, + stylesheets=[weasyprint.CSS(settings.STATIC_ROOT + 'css/bootstrap.min.css')]) + + # weasyprint.HTML(string=rendered_html, base_url=request.build_absolute_uri()).write_pdf(response, + # stylesheets=[weasyprint.CSS(settings.STATIC_ROOT + '/css/bootstrap.min.css')]) + mail_send.attach('order_{}.pdf'.format(order_id), out.getvalue(), 'application/pdf') + mail_send.send() + return mail_send 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_filters.py b/cart/templatetags/cart_filters.py new file mode 100644 index 0000000..4a3b1eb --- /dev/null +++ b/cart/templatetags/cart_filters.py @@ -0,0 +1,18 @@ +from django.template import Library + +register = Library() + + +@register.filter +def calculate_price(cart, offer): + # @TODO: BUG!!! MAKE TYPE CASTING OF CART KEYS + if offer.product_id in cart: + return offer.price * cart[offer.product_id]['quantity'] + return offer.price + + +@register.filter +def get_cart_offer_amount(cart, offer): + if offer.product_id in cart: + return cart[offer.product_id]['quantity'] if offer.product_id in cart else 0 + return 0 diff --git a/cart/templatetags/cart_tags.py b/cart/templatetags/cart_tags.py new file mode 100644 index 0000000..b696cb5 --- /dev/null +++ b/cart/templatetags/cart_tags.py @@ -0,0 +1,124 @@ +from django import template +from django.template import loader, Node, Variable +from django.utils.encoding import smart_str, smart_bytes +from django.template.defaulttags import url +from django.template import VariableDoesNotExist +from mptt.templatetags.mptt_tags import recursetree + +register = template.Library() + +@register.tag +def breadcrumb(parser, token): + """ + Renders the breadcrumb. + Examples: + {% breadcrumb "Title of breadcrumb" url_var %} + {% breadcrumb context_var url_var %} + {% breadcrumb "Just the title" %} + {% breadcrumb just_context_var %} + + Parameters: + -First parameter is the title of the crumb, + -Second (optional) parameter is the url variable to link to, produced by url tag, i.e.: + {% url person_detail object.id as person_url %} + then: + {% breadcrumb person.name person_url %} + + @author Andriy Drozdyuk + """ + return BreadcrumbNode(token.split_contents()[1:]) + + +@register.tag +def breadcrumb_url(parser, token): + """ + Same as breadcrumb + but instead of url context variable takes in all the + arguments URL tag takes. + {% breadcrumb "Title of breadcrumb" person_detail person.id %} + {% breadcrumb person.name person_detail person.id %} + """ + + bits = token.split_contents() + if len(bits)==2: + return breadcrumb(parser, token) + + # Extract our extra title parameter + title = bits.pop(1) + token.contents = ' '.join(bits) + + url_node = url(parser, token) + + return UrlBreadcrumbNode(title, url_node) + +@register.tag +def breadcrumb_mptt_url(parser, token): + return recursetree(parser, token) + +class BreadcrumbNode(Node): + def __init__(self, vars): + """ + First var is title, second var is url context variable + """ + self.vars = map(Variable,vars) + + def render(self, context): + title = self.vars[0].var + + if title.find("'")==-1 and title.find('"')==-1: + try: + val = self.vars[0] + title = val.resolve(context) + except: + title = '' + + else: + title=title.strip("'").strip('"') + title=smart_bytes(title) + + url = None + + if len(self.vars)>1: + val = self.vars[1] + try: + url = val.resolve(context) + except VariableDoesNotExist: + print('URL does not exist', val) + url = None + + return create_crumb(title, url) + + +class UrlBreadcrumbNode(Node): + def __init__(self, title, url_node): + self.title = Variable(title) + self.url_node = url_node + + def render(self, context): + title = self.title.var + + if title.find("'")==-1 and title.find('"')==-1: + try: + val = self.title + title = val.resolve(context) + except: + title = '' + else: + title=title.strip("'").strip('"') + title=smart_bytes(title) + + url = self.url_node.render(context) + return create_crumb(title, url) + + +def create_crumb(title, url=None): + """ + Helper function + """ + crumb = """




