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 = """
  • %s""" + if url: + crumb = crumb.format(url, title) + else: + crumb = crumb.format('#', title) + + return crumb diff --git a/cart/urls.py b/cart/urls.py index 8e77ac2..11a949a 100644 --- a/cart/urls.py +++ b/cart/urls.py @@ -4,9 +4,17 @@ from django.urls import re_path from . import views urlpatterns = [ - # url(r'^$', views.CartDetail, name='CartDetail'), - # url(r'^remove/(?P[-\w]+)/$', views.CartRemove, name='CartRemove'), - # url(r'^add/$', views.CartAdd, name='CartAdd'), re_path(r'^history/', views.BuyingsHistory.as_view(), name='history'), re_path(r'^buyings/$', views.CartView.as_view(), name='buyings'), + + re_path(r'^add/$', views.CartAddView.as_view(), name='add'), + re_path(r'^remove/$', views.CartRemoveView.as_view(), name='remove'), + re_path(r'^checkout/$', views.CartCheckoutView.as_view(),name='checkout'), + re_path(r'^confirm/$', views.CartConfirmView.as_view(), name='confirm'), + + # discount: @TODO: check if this logic is ready for production + re_path(r'^apply', views.DiscountApply, name='apply'), + re_path(r'^create', views.CreateDiscount, name='create'), + re_path(r'^points', views.PointsApply, name='points'), + re_path(r'^revoke_points', views.PointsRevoke, name='revoke_points') ] diff --git a/cart/utils.py b/cart/utils.py new file mode 100644 index 0000000..3463146 --- /dev/null +++ b/cart/utils.py @@ -0,0 +1,65 @@ +from decimal import Decimal +from django.conf import settings +from django.contrib import auth + +from cart.models import Offer +from products.models import Product + + +# from discount.models import Discount + +class Cart(object): + def __init__(self, request): + self.store = request.session + cart = self.store.get(settings.CART_SESSION_ID) + if not cart: + cart = self.store[settings.CART_SESSION_ID] = {} + self.cart = cart + + def add(self, offer_id, quantity=1): + offer_id = str(offer_id) + if offer_id in self.cart: + self.cart[offer_id]['quantity'] += int(quantity) + else: + self.cart[offer_id] = {'quantity': quantity} + self.save() + + def save(self): + self.store[settings.CART_SESSION_ID] = self.cart + self.store.modified = True + + def remove(self, offer_id): + if offer_id in self.cart: + del self.cart[offer_id] + self.save() + + def clear(self): + del self.store[settings.CART_SESSION_ID] + self.store.modified = True + + def keys(self): + return self.cart.keys() + + def __iter__(self): + return iter(self.cart.keys()) + + def __setitem__(self, key, value): + try: + self.cart[str(key)] = value + except KeyError: + return setattr(self, key, value) + + def __getitem__(self, key): + try: + return self.cart[str(key)] + except KeyError: + return getattr(self, key) + + def __contains__(self, item): + return str(item) in self.cart + + def __next__(self): + return self.cart.__next__() + + def __len__(self): + return self.cart.__len__() diff --git a/cart/views.py b/cart/views.py index 2d9898c..65e0314 100644 --- a/cart/views.py +++ b/cart/views.py @@ -1,82 +1,102 @@ +import datetime +import uuid +from functools import reduce + +from decimal import Decimal from django.conf import settings -from django.shortcuts import render, redirect, get_object_or_404 -from django.views.decorators.http import require_POST -from django.views.decorators.csrf import csrf_exempt -from django.contrib.auth.decorators import login_required -from django.contrib import auth -from django.views.generic import ListView +from django.shortcuts import redirect +from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ +from django.views.generic.edit import BaseFormView + +from cart.utils import Cart +from cart.models import Buying, Discount, Offer +from core.views import ProtectedListView, ProtectedTemplateView, ProtectedView, ProtectedFormView + +from .forms import ( + CartRemoveBuyingForm, + DiscountForm, + CartAddInlineForm, CartCheckoutForm +) + + +class CartAddView(BaseFormView): + http_method_names = ('post',) + form_class = CartAddInlineForm + + def get_success_url(self): + return self.request.META.get('HTTP_REFERRER', reverse_lazy('products:product_list')) + + def form_valid(self, form): + self.request.cart.add(offer_id=form.cleaned_data['offer'].product_id, quantity=form.cleaned_data['amount']) + return super().form_valid(form) + + +class CartRemoveView(BaseFormView): + http_method_names = ('post',) + model = Offer + form_class = CartRemoveBuyingForm + + def get_success_url(self): + return self.request.META.get('HTTP_REFERRER', reverse_lazy('products:product_list')) -from cart.models import Buying -from core.views import ProtectedListView - -from .cart import Cart -from .forms import CartAddProductForm - - -# from discount.layout import DiscountApllyForm - -# @csrf_exempt -# @require_POST -# @login_required(login_url='accounts_ext:accounts_ext') -# def CartAdd(request): -# cart = Cart(request) -# form = CartAddProductForm(request.POST) -# if form.is_valid(): -# cd = form.cleaned_data -# if int(cd['quantity']) < 1 or int(cd['quantity']) > 1000: -# return redirect(request.META.get('HTTP_REFERER')) -# offer = get_object_or_404(Offer, slug=cd['product_slug']) -# cart.add(offer=offer, price_per_itom=cd['price_per_itom'], quantity=cd['quantity'], -# update_quantity=cd['update']) -# request.session.pop('points', None) -# return redirect('cart:CartDetail') -# -# @csrf_exempt -# @login_required(login_url='accounts_ext:accounts_ext') -# def CartRemove(request, offer_slug): -# cart = Cart(request) -# # offer = get_object_or_404(Offer, slug=offer_slug) -# cart.remove(offer_slug) -# request.session.pop('points', None) -# return redirect('cart:CartDetail') -# -# @csrf_exempt -# @login_required(login_url='accounts_ext:accounts_ext') -# def CartDetail(request): -# user = auth.get_user(request) -# cart = Cart(request) -# for item in cart: -# item['update_quantity_form'] = CartAddProductForm( -# initial={ -# 'quantity': item['quantity'], -# 'product_slug': item['offer'].slug, -# 'price_per_itom': item['price'], -# 'update': True -# }) -# # discount_apply_form = DiscountApllyForm() -# return render(request, 'cart/detail.html', {'username': user.username}) -# # 'discount_apply_form': discount_apply_form}) + def form_valid(self, form): + self.request.cart.remove(form.cleaned_data['offer']) + return super().form_valid() class CartView(ProtectedListView): - model = Buying + model = Offer paginate_by = settings.DEFAULT_PAGE_AMOUNT - context_object_name = 'cart_items' + context_object_name = 'offer_items' template_name = 'cart/cart.html' ordering = '-create_at' title = _('Корзина') def get_queryset(self): qs = super().get_queryset() - return qs.filter(user=self.request.user) + return qs.filter(product__id__in=self.request.cart.keys()) + + def get_total_price(self, object_list): + return reduce( + lambda initial, offer: initial + offer.price * Decimal(self.request.cart[offer.product_id]['quantity']), + object_list, + Decimal(0) + ) + + def get_total_cashback(self, object_list): + return reduce( + lambda initial, offer: initial + offer.cashback * Decimal(self.request.cart[offer.product_id]['quantity']), + object_list, + Decimal(0) + ) def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['title'] = self.title + context['total_price'] = self.get_total_price(self.object_list) + context['total_price_currency'] = context['total_cashback_currency'] = self.object_list.first().currency.sign + context['total_cashback'] = self.get_total_cashback(self.object_list) return context +class CartCheckoutView(ProtectedFormView): + http_method_names = ('get', 'post',) + template_name = 'cart/checkout.html' + form_class = CartCheckoutForm + + def get_form_kwargs(self): + return super().get_form_kwargs() + + def form_valid(self, form): + return super().form_valid(form) + + +class CartConfirmView(ProtectedTemplateView): + http_method_names = ('post',) + template_name = 'cart/confirm.html' + + class BuyingsHistory(ProtectedListView): model = Buying paginate_by = settings.DEFAULT_PAGE_AMOUNT @@ -93,3 +113,65 @@ class BuyingsHistory(ProtectedListView): context = super().get_context_data(object_list=object_list, **kwargs) context['title'] = self.title return context + +# Discount views # @TODO: TEST FOR PRODUCTION +class PointsApply(ProtectedTemplateView): + http_method_names = ('post',) + + def dispatch(self, request, *args, **kwargs): + super().dispatch(request, *args, **kwargs) + self.request.session['points'] = True + return redirect('cart:buyings') + + + + +class PointsRevoke(ProtectedView): + http_method_names = ('post',) + + def dispatch(self, request, *args, **kwargs): + super().dispatch(request, *args, **kwargs) + self.request.session.pop('points', None) + return redirect('cart:buyings') + + +class DiscountApply(ProtectedFormView): + http_method_names = ('post',) + + form_class = DiscountForm + + def form_valid(self, form): + now = datetime.now() + code = form.cleaned_data['code'] + try: + discount = Discount.objects.get(code__iexact=code, + valid_from__lte=now, + valid_to__gte=now, + active=True) + self.request.session['discount_id'] = discount.id + except Discount.DoesNotExist: + self.request.session['discount_id'] = None + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy('cart:buyings') + + +class CreateDiscount(ProtectedFormView): + http_method_names = ('post',) + + form_class = DiscountForm + + def form_valid(self, form): + now = datetime.now() + Discount.objects.update_or_create( + user=self.request.user, + defaults={'code': str(uuid.uuid4()), + 'valid_from': now, + 'valid_to': now + datetime.timedelta(days=7), + 'active': True} + ) + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy('cabinet:index') diff --git a/templates/cart/bought_history.html b/templates/cart/bought_history.html index 8f23bee..eb9510e 100644 --- a/templates/cart/bought_history.html +++ b/templates/cart/bought_history.html @@ -25,32 +25,41 @@ {{ bought_item.total_price }}7570₽ {% empty %} - - 12.06.17 - 142251366 - Windows 7 BOX... - 1 - 7570₽ - {% endfor %} - + {% if paginator.num_pages > 1 %} + {% spaceless %} + + {% endspaceless %} + {% endif %} {% endblock content %} diff --git a/templates/cart/buying_history.html b/templates/cart/buying_history.html deleted file mode 100644 index 4e78f8b..0000000 --- a/templates/cart/buying_history.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends 'base.html' %} -{% block content %} - Missing buying history -{% endblock %} diff --git a/templates/cart/cart.html b/templates/cart/cart.html index 741897b..d2d921c 100644 --- a/templates/cart/cart.html +++ b/templates/cart/cart.html @@ -1,4 +1,8 @@ {% extends 'base.html' %} +{% load core_filters %} +{% load cart_filters %} + + {% block content %}
    @@ -20,149 +24,55 @@
    -
    -
    -
    -
    -
    -
    - -
    Microsoft Windows 7 Professional SP1 - (x32/x64) GGK [6PC-00024] + {% for offer in offer_items %} +
    +
    +
    +
    +
    +
    + +
    {{ offer.product.name }}
    -
    -
    -
    -
    -
    Цена
    -
    10 000 ₽
    -
    -
    -
    Количество
    -
    2 шт
    -
    -
    -
    Сумма
    -
    20 000 ₽
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    Microsoft Windows 7 Professional SP1 - (x32/x64) GGK [6PC-00024] +
    +
    +
    +
    Цена
    +
    {{ offer.get_price_with_discount }} {{ offer.currency.sign }}
    -
    -
    -
    -
    -
    -
    -
    Цена
    -
    10 000 ₽
    -
    -
    -
    Количество
    -
    2 шт
    -
    -
    -
    Сумма
    -
    20 000 ₽
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    Microsoft Windows 7 Professional SP1 - (x32/x64) GGK [6PC-00024] +
    +
    Количество
    +
    {{ request.cart|get_cart_offer_amount:offer}}
    -
    -
    -
    -
    -
    -
    -
    Цена
    -
    10 000 ₽
    -
    -
    -
    Количество
    -
    2 шт
    -
    -
    -
    Сумма
    -
    20 000 ₽
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    Microsoft Windows 7 Professional SP1 - (x32/x64) GGK [6PC-00024] +
    +
    Сумма
    +
    {{ request.cart|calculate_price:offer }} {{ offer.currency.sign }}
    -
    -
    -
    -
    Цена
    -
    10 000 ₽
    -
    -
    -
    Количество
    -
    2 шт
    -
    -
    -
    Сумма
    -
    20 000 ₽
    -
    -
    +
    + {% empty %} +
    +
    + Ваша корзина еще пуста:(( Вернитесь в категории и купите что-нибудь
    -
    + {% endfor %}
    -
    Итого: 20 000 ₽
    -
    Кэшбек 800 ₽
    +
    Итого: {{ total_price }} {{ total_price_currency }}
    + {% if total_cashback > 0 %} +
    Кэшбек {{ total_cashback }} {{ total_cashback_currency }}
    + {% endif %}
    diff --git a/templates/cart/checkout.html b/templates/cart/checkout.html new file mode 100644 index 0000000..21f5da2 --- /dev/null +++ b/templates/cart/checkout.html @@ -0,0 +1 @@ +{% extends 'base.html' %} diff --git a/templates/cart/confirm.html b/templates/cart/confirm.html new file mode 100644 index 0000000..21f5da2 --- /dev/null +++ b/templates/cart/confirm.html @@ -0,0 +1 @@ +{% extends 'base.html' %}