update cart app

remotes/origin/HEAD
Max Yakovenko 8 years ago
parent 0547ed23ba
commit 4f26a4b7c3
  1. 142
      cart/admin.py
  2. 87
      cart/cart.py
  3. 6
      cart/context_processors.py
  4. 1
      cart/fixtures/supply_targets.json
  5. 1
      cart/fixtures/supply_types.json
  6. 272
      cart/forms.py
  7. 13
      cart/middleware.py
  8. 274
      cart/models.py
  9. 45
      cart/tasks.py
  10. 0
      cart/templatetags/__init__.py
  11. 18
      cart/templatetags/cart_filters.py
  12. 124
      cart/templatetags/cart_tags.py
  13. 14
      cart/urls.py
  14. 65
      cart/utils.py
  15. 200
      cart/views.py
  16. 57
      templates/cart/bought_history.html
  17. 4
      templates/cart/buying_history.html
  18. 168
      templates/cart/cart.html
  19. 1
      templates/cart/checkout.html
  20. 1
      templates/cart/confirm.html

@ -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',)

@ -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

@ -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) }

@ -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}}]

@ -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',)

@ -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

@ -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)

@ -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

@ -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

@ -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 = """<li><a href='%s'>%s</a></span>"""
if url:
crumb = crumb.format(url, title)
else:
crumb = crumb.format('#', title)
return crumb

@ -4,9 +4,17 @@ from django.urls import re_path
from . import views
urlpatterns = [
# url(r'^$', views.CartDetail, name='CartDetail'),
# url(r'^remove/(?P<offer_slug>[-\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')
]

@ -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__()

@ -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')

@ -25,32 +25,41 @@
<td>{{ bought_item.total_price }}7570₽</td>
</tr>
{% empty %}
<tr>
<td>12.06.17</td>
<td>142251366</td>
<td>Windows 7 BOX...</td>
<td>1</td>
<td>7570₽</td>
</tr>
{% endfor %}
</table>
<div class="pagination">
<span class="step-links">
{% if bought_item_list.has_previous %}
<a href="?page=1">&laquo; 1</a>
<a href="?page={{ bought_item_list.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Страница {{ bought_item_list.number }} of {{ bought_item_list.paginator.num_pages }}.
</span>
{% if contacts.has_next %}
<a href="?page={{ bought_item_list.next_page_number }}">next</a>
<a href="?page={{ bought_item_list.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</div>
{% if paginator.num_pages > 1 %}
{% spaceless %}
<div class="row pagination">
<div class="col-md-4 text-center">
{% if page_obj.has_previous %}
<a href="{% url 'cart:history' %}?page={{ page_obj.previous_page_number }}">
&laquo;
</a>
{% endif %}
</div>
<div class="col-md-4 text-center">
{% if paginator.has_previous %}
<a href="{% url 'cart:history' %}?page={{ page_obj.number|add:"-1" }}">
{{ page_obj.number|add:"-1" }}
</a>
{% endif %}
<span>{{ page_obj.number }}</span>
{% if page_obj.has_next %}
<a href="{% url 'cart:history' %}?page={{ page_obj.number|add:"+1" }}">
{{ page_obj.number|add:"+1" }}
</a>
{% endif %}
</div>
<div class="col-md-4 text-center">
{% if page_obj.has_next %}
<a href="{% url 'cart:history' %}?page={{ page_obj.next_page_number }}">
&raquo;
</a>
{% endif %}
</div>
</div>
{% endspaceless %}
{% endif %}
</div>
</div>
{% endblock content %}

@ -1,4 +0,0 @@
{% extends 'base.html' %}
{% block content %}
Missing buying history
{% endblock %}

@ -1,4 +1,8 @@
{% extends 'base.html' %}
{% load core_filters %}
{% load cart_filters %}
{% block content %}
<div class="col-12">
<div class="content">
@ -20,149 +24,55 @@
</div>
</div>
</div>
<div class="basket__item">
<div class="row align-items-center">
<div class="col-md-6 col-12">
<div class="basket__good d-flex align-items-center">
<div class="basket__image"><img src="./img/item-image-min.png" alt=""></div>
<div class="basket__info">
<ul class="basket__breadcrumbs breadcrumbs">
<li>Товары</li>
<li>Антивирусы</li>
</ul>
<div class="basket__good-name">Microsoft Windows 7 Professional SP1
(x32/x64) GGK [6PC-00024]
{% for offer in offer_items %}
<div class="basket__item">
<div class="row align-items-center">
<div class="col-md-6 col-12">
<div class="basket__good d-flex align-items-center">
<div class="basket__image"><img src="./img/item-image-min.png" alt=""></div>
<div class="basket__info">
<ul class="basket__breadcrumbs breadcrumbs">
<li><a href="{% url 'products:product_list' %}">Категории</a></li>
<li><a href="{{ offer.product.get_absolute_url }}">{{ offer.product.name }}</a></li>
</ul>
<div class="basket__good-name">{{ offer.product.name }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-12 basket__option">
<div class="row">
<div class="col-4 align-self-start">
<div class="basket__title-mob">Цена</div>
<div class="basket__price">10 000 ₽</div>
</div>
<div class="col-4 align-self-start">
<div class="basket__title-mob align-self-start">Количество</div>
<div class="basket__count">2 шт</div>
</div>
<div class="col-4 align-self-start">
<div class="basket__title-mob ">Сумма</div>
<div class="basket__sum">20 000 ₽</div>
</div>
</div>
</div>
</div>
</div>
<div class="basket__item">
<div class="row align-items-center">
<div class="col-md-6 col-12">
<div class="basket__good d-flex align-items-center">
<div class="basket__image"><img src="./img/item-image-min.png" alt=""></div>
<div class="basket__info">
<ul class="basket__breadcrumbs breadcrumbs">
<li>Товары</li>
<li>Антивирусы</li>
</ul>
<div class="basket__good-name">Microsoft Windows 7 Professional SP1
(x32/x64) GGK [6PC-00024]
<div class="col-md-6 col-12 basket__option">
<div class="row">
<div class="col-4 align-self-start">
<div class="basket__title-mob">Цена</div>
<div class="basket__price">{{ offer.get_price_with_discount }} {{ offer.currency.sign }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-12 basket__option">
<div class="row">
<div class="col-4 align-self-start">
<div class="basket__title-mob">Цена</div>
<div class="basket__price">10 000 ₽</div>
</div>
<div class="col-4 align-self-start">
<div class="basket__title-mob align-self-start">Количество</div>
<div class="basket__count">2 шт</div>
</div>
<div class="col-4 align-self-start">
<div class="basket__title-mob ">Сумма</div>
<div class="basket__sum">20 000 ₽</div>
</div>
</div>
</div>
</div>
</div>
<div class="basket__item">
<div class="row align-items-center">
<div class="col-md-6 col-12">
<div class="basket__good d-flex align-items-center">
<div class="basket__image"><img src="./img/item-image-min.png" alt=""></div>
<div class="basket__info">
<ul class="basket__breadcrumbs breadcrumbs">
<li>Товары</li>
<li>Антивирусы</li>
</ul>
<div class="basket__good-name">Microsoft Windows 7 Professional SP1
(x32/x64) GGK [6PC-00024]
<div class="col-4 align-self-start">
<div class="basket__title-mob align-self-start">Количество</div>
<div class="basket__count">{{ request.cart|get_cart_offer_amount:offer}}</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-12 basket__option">
<div class="row">
<div class="col-4 align-self-start">
<div class="basket__title-mob">Цена</div>
<div class="basket__price">10 000 ₽</div>
</div>
<div class="col-4 align-self-start">
<div class="basket__title-mob align-self-start">Количество</div>
<div class="basket__count">2 шт</div>
</div>
<div class="col-4 align-self-start">
<div class="basket__title-mob ">Сумма</div>
<div class="basket__sum">20 000 ₽</div>
</div>
</div>
</div>
</div>
</div>
<div class="basket__item">
<div class="row align-items-center">
<div class="col-md-6 col-12">
<div class="basket__good d-flex align-items-center">
<div class="basket__image"><img src="./img/item-image-min.png" alt=""></div>
<div class="basket__info">
<ul class="basket__breadcrumbs breadcrumbs">
<li>Товары</li>
<li>Антивирусы</li>
</ul>
<div class="basket__good-name">Microsoft Windows 7 Professional SP1
(x32/x64) GGK [6PC-00024]
<div class="col-4 align-self-start">
<div class="basket__title-mob ">Сумма</div>
<div class="basket__sum">{{ request.cart|calculate_price:offer }} {{ offer.currency.sign }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-12 basket__option">
<div class="row">
<div class="col-4 align-self-start">
<div class="basket__title-mob">Цена</div>
<div class="basket__price">10 000 ₽</div>
</div>
<div class="col-4 align-self-start">
<div class="basket__title-mob align-self-start">Количество</div>
<div class="basket__count">2 шт</div>
</div>
<div class="col-4 align-self-start">
<div class="basket__title-mob ">Сумма</div>
<div class="basket__sum">20 000 ₽</div>
</div>
</div>
</div>
{% empty %}
<div class="basket__item">
<div class="row align-items-center">
Ваша корзина еще пуста:(( Вернитесь в категории и купите что-нибудь
</div>
</div>
</div>
{% endfor %}
<div class="basket__result">
<div class="result__sum">Итого: 20 000 ₽</div>
<div class="result__cashback">Кэшбек 800 ₽</div>
<div class="result__sum">Итого: {{ total_price }} {{ total_price_currency }}</div>
{% if total_cashback > 0 %}
<div class="result__cashback">Кэшбек {{ total_cashback }} {{ total_cashback_currency }}</div>
{% endif %}
</div>
<div class="basket__controls">
<a class="basket__btn-fill" href="#">Оформить</a>
<a class="basket__btn" href="#">Продолжить покупки</a>
<a class="basket__btn-fill" href="{% url 'cart:checkout' %}">Оформить</a>
<a class="basket__btn" href="{% url 'products:product_list' %}">Продолжить покупки</a>
</div>
</div>
</div>

@ -0,0 +1 @@
{% extends 'base.html' %}

@ -0,0 +1 @@
{% extends 'base.html' %}
Loading…
Cancel
Save