documents are being sent properly, store models has changed a bit, added a lot of tests

pull/2/head
mtyton 2023-06-18 16:18:00 +02:00
rodzic 000b911f89
commit 8bf933a469
23 zmienionych plików z 741 dodań i 301 usunięć

Wyświetl plik

@ -184,4 +184,5 @@ EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com')
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", '')
EMAIL_PORT = os.environ.get('EMAIL_PORT', 587)
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'mtyton@tepewu.pl')

Wyświetl plik

@ -9,7 +9,13 @@ SECRET_KEY = "django-insecure-s7hlfa-#n7-v0#&-0ko3(efe+@^d@ie1_1-633e&jb1rh$)j1p
# SECURITY WARNING: define the correct hosts in production!
ALLOWED_HOSTS = ["*"]
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = "smtp-server"
EMAIL_HOST_USER = None
EMAIL_HOST_PASSWORD = None
EMAIL_PORT = 1025
EMAIL_USE_TLS = False
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'mtyton@tepewu.pl')
try:

Wyświetl plik

@ -1,5 +1,10 @@
version: "3.8"
services:
smtp-server:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"
comfy:
build:
dockerfile: Dockerfile.local

Wyświetl plik

@ -7,4 +7,5 @@ djangorestframework==3.14.0
phonenumbers==8.13.13
django-phonenumber-field==7.1.0
factory-boy==3.2.1
pdfkit==1.0.0
pdfkit==1.0.0
num2words==0.5.12

Wyświetl plik

@ -35,10 +35,14 @@ class ProductAdmin(ModelAdmin):
list_display = ("title", "price")
class PaymentMethodAdmin(ModelAdmin):
model = models.PaymentMethod
list_display = ("name", "active")
class DocumentTemplateAdmin(ModelAdmin):
model = models.DocumentTemplate
list_display = ("name", "doc_type")
list_display = ("name", )
class StoreAdminGroup(ModelAdminGroup):
@ -51,7 +55,8 @@ class StoreAdminGroup(ModelAdminGroup):
ProductCategoryParamAdmin,
ProductTemplateAdmin,
ProductAdmin,
DocumentTemplateAdmin
DocumentTemplateAdmin,
PaymentMethodAdmin
)

Wyświetl plik

@ -2,23 +2,23 @@ from abc import (
ABC,
abstractmethod
)
from typing import List
from typing import (
List,
Any
)
from dataclasses import dataclass
from django.http.request import HttpRequest
from django.conf import settings
from store.models import Product
@dataclass
class CartItem:
product: Product
quantity: int
from store.models import (
Product,
ProductAuthor
)
class BaseCart(ABC):
def validate_item_id(self, item_id):
def validate_and_get_product(self, item_id):
return Product.objects.get(id=item_id)
@abstractmethod
@ -43,57 +43,76 @@ class SessionCart(BaseCart):
def __init__(self, request: HttpRequest) -> None:
super().__init__()
self.session = request.session
if not self.session.get(settings.CART_SESSION_ID):
self.session[settings.CART_SESSION_ID] = {}
self._cart = self.session.get(settings.CART_SESSION_ID, None)
if not self._cart:
self._cart = {}
self.session[settings.CART_SESSION_ID] = self._cart
def save_cart(self):
self.session[settings.CART_SESSION_ID] = self._cart
self.session.modified = True
def add_item(self, item_id: int, quantity: int) -> None:
# TODO - add logging
self.validate_item_id(item_id)
product = self.validate_and_get_product(item_id)
author = product.author
quantity = int(quantity)
item_id = int(item_id)
if not self.session[settings.CART_SESSION_ID].get(str(item_id)):
self.session[settings.CART_SESSION_ID][item_id] = quantity
self.session.modified = True
if not self._cart.get(str(author.id)):
self._cart[str(author.id)] = {str(item_id): quantity}
self.save_cart()
elif not self._cart[str(author.id)].get(str(item_id)):
self._cart[str(author.id)].update({str(item_id): quantity})
self.save_cart()
else:
self.update_item_quantity(item_id, quantity)
new_quantity = self._cart[str(author.id)][str(item_id)] + quantity
self.update_item_quantity(item_id, new_quantity)
def remove_item(self, item_id: int) -> None:
self.validate_item_id(item_id)
product = self.validate_and_get_product(item_id)
author = product.author
try:
self.session[settings.CART_SESSION_ID].pop(item_id)
self.session.modified = True
self._cart[str(author.id)].pop(str(item_id))
self.save_cart()
except KeyError:
# TODO - add logging
...
def update_item_quantity(self, item_id: int, new_quantity: int) -> None:
self.validate_item_id(item_id)
product = self.validate_and_get_product(item_id)
author = product.author
if new_quantity < 1:
self.remove_item(item_id)
return
try:
self.session[settings.CART_SESSION_ID][str(item_id)] = new_quantity
self.session.modified = True
except KeyError:
# TODO - add logging
if not self._cart.get(str(author.id)):
self.add_item(item_id, new_quantity)
return
self._cart[str(author.id)][str(product.id)] = new_quantity
self.save_cart()
def get_items(self) -> List[CartItem]:
_items = []
for item_id, quantity in self.session[settings.CART_SESSION_ID].items():
_items.append(CartItem(quantity=quantity, product=Product.objects.get(id=item_id)))
return _items
def get_items(self) -> List[dict[str, dict|str]]:
items: List[dict[str, dict|str]] = []
for author_id, cart_items in self._cart.items():
author = ProductAuthor.objects.get(id=int(author_id))
products = []
for item_id, quantity in cart_items.items():
product=Product.objects.get(id=int(item_id))
products.append({"product": product, "quantity": quantity})
items.append({"author": author, "products": products})
return items
@property
def total_price(self):
total = 0
for item in self.get_items():
total += item.product.price * int(item.quantity)
for _, cart_items in self._cart.items():
for item_id, quantity in cart_items.items():
product = Product.objects.get(id=int(item_id))
total += product.price * quantity
return total
def is_empty(self) -> bool:
return not bool(self.session[settings.CART_SESSION_ID].items())
return not bool(self._cart.items())
def clear(self) -> None:
self.session[settings.CART_SESSION_ID] = {}
self.session.modified = True
self._cart = {}
self.save_cart()

Wyświetl plik

@ -2,18 +2,9 @@ from django import forms
from phonenumber_field.formfields import PhoneNumberField
# from phonenumber_field.widgets import PhoneNumberPrefixWidget
from store.models import (
CustomerData,
)
class CustomerDataForm(forms.ModelForm):
class Meta:
model = CustomerData
fields = [
"name", "surname", "email", "phone",
"street", "city", "zip_code"
]
class CustomerDataForm(forms.Form):
name = forms.CharField(
max_length=255, label="Imię", widget=forms.TextInput(attrs={"class": "form-control"})

Wyświetl plik

@ -1,53 +0,0 @@
# Generated by Django 4.1.9 on 2023-06-01 19:00
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
("store", "0003_product_info_product_name_and_more"),
]
operations = [
migrations.CreateModel(
name="CustomerData",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
("surname", models.CharField(max_length=255)),
("email", models.EmailField(max_length=254)),
("phone", phonenumber_field.modelfields.PhoneNumberField(max_length=128, region=None)),
("street", models.CharField(max_length=255)),
("city", models.CharField(max_length=255)),
("zip_code", models.CharField(max_length=120)),
("country", models.CharField(max_length=120)),
],
),
migrations.CreateModel(
name="Order",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("sent", models.BooleanField(default=False)),
("customer", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.customerdata")),
],
),
migrations.CreateModel(
name="OrderProduct",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("quantity", models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="products", to="store.order"
),
),
("product", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.product")),
],
),
]

Wyświetl plik

@ -0,0 +1,129 @@
# Generated by Django 4.1.9 on 2023-06-16 15:33
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
("store", "0003_product_info_product_name_and_more"),
]
operations = [
migrations.CreateModel(
name="DocumentTemplate",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
("file", models.FileField(upload_to="documents")),
(
"doc_type",
models.CharField(
choices=[("agreement", "Agreement"), ("receipt", "Receipt")], max_length=255, unique=True
),
),
("created_at", models.DateTimeField(auto_now_add=True, null=True)),
],
),
migrations.CreateModel(
name="Order",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("sent", models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name="PaymentMethod",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
("description", models.TextField(blank=True)),
("active", models.BooleanField(default=True)),
],
),
migrations.AddField(
model_name="productauthor",
name="city",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="productauthor",
name="country",
field=models.CharField(blank=True, max_length=120),
),
migrations.AddField(
model_name="productauthor",
name="display_name",
field=models.CharField(blank=True, max_length=255, unique=True),
),
migrations.AddField(
model_name="productauthor",
name="email",
field=models.EmailField(blank=True, max_length=254),
),
migrations.AddField(
model_name="productauthor",
name="phone",
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None),
),
migrations.AddField(
model_name="productauthor",
name="street",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="productauthor",
name="surname",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="productauthor",
name="zip_code",
field=models.CharField(blank=True, max_length=120),
),
migrations.AlterField(
model_name="productauthor",
name="name",
field=models.CharField(blank=True, max_length=255),
),
migrations.CreateModel(
name="OrderProduct",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("quantity", models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="products", to="store.order"
),
),
("product", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.product")),
],
),
migrations.CreateModel(
name="OrderDocument",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("sent", models.BooleanField(default=False)),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="store.order"
),
),
(
"template",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.documenttemplate"),
),
],
),
migrations.AddField(
model_name="order",
name="payment_method",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.paymentmethod"),
),
]

Wyświetl plik

@ -1,41 +0,0 @@
# Generated by Django 4.1.9 on 2023-06-04 09:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("store", "0004_customerdata_order_orderproduct"),
]
operations = [
migrations.CreateModel(
name="DocumentTemplate",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
("file", models.FileField(upload_to="documents")),
(
"doc_type",
models.CharField(choices=[("agreement", "Agreement"), ("receipt", "Receipt")], max_length=255),
),
],
),
migrations.CreateModel(
name="OrderDocument",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="store.order"
),
),
(
"template",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.documenttemplate"),
),
],
),
]

Wyświetl plik

@ -0,0 +1,17 @@
# Generated by Django 4.1.9 on 2023-06-18 11:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("store", "0004_documenttemplate_order_paymentmethod_and_more"),
]
operations = [
migrations.AddField(
model_name="order",
name="order_number",
field=models.CharField(max_length=255, null=True),
),
]

Wyświetl plik

@ -1,22 +0,0 @@
# Generated by Django 4.1.9 on 2023-06-07 23:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("store", "0005_documenttemplate_orderdocument"),
]
operations = [
migrations.AddField(
model_name="documenttemplate",
name="created_at",
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name="orderdocument",
name="sent",
field=models.BooleanField(default=False),
),
]

Wyświetl plik

@ -1,4 +1,8 @@
import pdfkit
import datetime
from decimal import Decimal
from typing import Any
from django.db import models
from django.core.paginator import (
Paginator,
@ -21,18 +25,42 @@ from wagtail.models import Page
from wagtail import fields as wagtail_fields
from taggit.managers import TaggableManager
from phonenumber_field.modelfields import PhoneNumberField
from num2words import num2words
from store.utils import (
send_mail
notify_user_about_order,
notify_manufacturer_about_order
)
class ProductAuthor(models.Model):
name = models.CharField(max_length=255)
# TODO - add author contact data
class PersonalData(models.Model):
class Meta:
abstract = True
name = models.CharField(max_length=255, blank=True)
surname = models.CharField(max_length=255, blank=True)
email = models.EmailField(blank=True)
phone = PhoneNumberField(blank=True)
street = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=255, blank=True)
zip_code = models.CharField(max_length=120, blank=True)
country = models.CharField(max_length=120, blank=True)
@property
def full_name(self):
return f"{self.name} {self.surname}"
@property
def full_address(self):
return f"{self.street}, {self.zip_code} {self.city}, {self.country}"
class ProductAuthor(PersonalData):
display_name = models.CharField(max_length=255, unique=True, blank=True)
def __str__(self):
return self.name
return self.display_name
class ProductCategory(ClusterableModel):
@ -111,7 +139,6 @@ class Product(ClusterableModel):
@property
def main_image(self):
images = self.template.images.all()
print(images)
if images:
return images.first().image
@ -119,6 +146,10 @@ class Product(ClusterableModel):
def tags(self):
return self.template.tags.all()
@property
def author(self):
return self.template.author
@property
def description(self):
return self.info or self.template.description
@ -166,33 +197,22 @@ class ProductListPage(Page):
FieldPanel("tags")
]
class CustomerData(models.Model):
name = models.CharField(max_length=255)
surname = models.CharField(max_length=255)
email = models.EmailField()
phone = PhoneNumberField()
street = models.CharField(max_length=255)
city = models.CharField(max_length=255)
zip_code = models.CharField(max_length=120)
country = models.CharField(max_length=120)
@property
def full_name(self):
return f"{self.name} {self.surname}"
@property
def full_address(self):
return f"{self.street}, {self.zip_code} {self.city}, {self.country}"
class OrderProductManager(models.Manager):
def create_from_cart(self, cart, order):
for item in cart.get_items():
self.create(
product=item.product,
def create_from_cart(self, items: dict[str, Product|int], order: models.Model):
pks = []
for item in items:
if item["quantity"] < 1:
# TODO - logging
continue
pk = self.create(
product=item["product"],
order=order,
quantity=item.quantity
)
quantity=item["quantity"]
).pk
pks.append(pk)
return self.filter(pk__in=pks)
class OrderProduct(models.Model):
@ -204,41 +224,107 @@ class OrderProduct(models.Model):
class OrderManager(models.Manager):
def create_from_cart(self, cart, customer_data):
order = self.create(customer=customer_data)
OrderProduct.objects.create_from_cart(cart, order)
# create proper documents
# NOTE - this is temporary
# agreement_template = DocumentTemplate.objects.filter(
# doc_type=DocumentTypeChoices.AGREEMENT
# ).order_by("-created_at").first()
# receipt_template = DocumentTemplate.objects.filter(
# doc_type=DocumentTypeChoices.RECEIPT
# ).order_by("-created_at").first()
# agreement = OrderDocument.objects.create(
# order=order,
# template=agreement_template
# )
# receipt = OrderDocument.objects.create(
# order=order,
# template=receipt_template
# )
#send_mail(agreement)
#send_mail(receipt)
return order
def _get_order_number(self, author: ProductAuthor):
number_of_prev_orders = OrderProduct.objects.filter(
product__template__author=author
).values("order").distinct().count()
number_of_prev_orders += 1
year = datetime.datetime.now().year
return f"{author.id}/{number_of_prev_orders:06}/{year}"
def create_from_cart(
self, cart_items: list[dict[str, str|dict]],
payment_method: models.Model| None,
customer_data: dict[str, Any]
) -> models.QuerySet:
# split cart
orders_pks = []
payment_method = payment_method or PaymentMethod.objects.first()
agreement_template = DocumentTemplate.objects.get(doc_type=DocumentTypeChoices.AGREEMENT)
receipt_template = DocumentTemplate.objects.get(doc_type=DocumentTypeChoices.RECEIPT)
for item in cart_items:
author = item["author"]
author_products = item["products"]
order = self.create(
payment_method=payment_method,
order_number=self._get_order_number(author)
)
OrderProduct.objects.create_from_cart(author_products, order)
orders_pks.append(order.pk)
agreement = OrderDocument.objects.create(
order=order,
template=agreement_template
)
receipt = OrderDocument.objects.create(
order=order,
template=receipt_template
)
extra_document_kwargs = {
"customer_data": customer_data
}
default_kwargs ={
"docs": [
agreement.generate_document(extra_document_kwargs),
receipt.generate_document(extra_document_kwargs)
],
"order_number": order.order_number
}
user_kwargs = {
"customer_email": customer_data["email"],
}
user_kwargs.update(default_kwargs)
user_notified = notify_user_about_order(**user_kwargs)
manufacturer_kwargs = {
"manufacturer_email": author.email,
}
manufacturer_kwargs.update(default_kwargs)
manufacturer_notified = notify_manufacturer_about_order(**manufacturer_kwargs)
sent = user_notified and manufacturer_notified
agreement.sent = sent
receipt.sent = sent
agreement.save()
receipt.save()
return Order.objects.filter(pk__in=orders_pks)
class PaymentMethod(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
active = models.BooleanField(default=True)
class Order(models.Model):
customer = models.ForeignKey(CustomerData, on_delete=models.CASCADE)
payment_method = models.ForeignKey(PaymentMethod, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
sent = models.BooleanField(default=False)
order_number = models.CharField(max_length=255, null=True)
objects = OrderManager()
@property
def order_number(self) -> str:
return f"{self.id:06}/{self.created_at.year}"
def manufacturer(self) -> str:
return self.products.first().product.author
@property
def total_price(self) -> Decimal:
return sum(
[order_product.product.price * order_product.quantity
for order_product in self.products.all()]
)
@property
def total_price_words(self) -> str:
return num2words(self.total_price, lang="pl", to="currency", currency="PLN")
@property
def payment_date(self) -> datetime.date:
return self.created_at.date() + datetime.timedelta(days=7)
class DocumentTypeChoices(models.TextChoices):
@ -249,7 +335,8 @@ class DocumentTypeChoices(models.TextChoices):
class DocumentTemplate(models.Model):
name = models.CharField(max_length=255)
file = models.FileField(upload_to="documents")
doc_type = models.CharField(max_length=255, choices=DocumentTypeChoices.choices)
# there may be only one document of each type
doc_type = models.CharField(max_length=255, choices=DocumentTypeChoices.choices, unique=True)
created_at = models.DateTimeField(auto_now_add=True, null=True)
def __str__(self):
@ -264,16 +351,19 @@ class OrderDocument(models.Model):
def get_document_context(self):
_context = {
"order": self.order,
"customer": self.order.customer,
"products": self.order.products.all(),
"author": self.order.manufacturer,
"order_products": self.order.products.all(),
"payment_data": self.order.payment_method,
}
return Context(_context)
@property
def document(self):
with open(self.template.file.path, "rb") as f:
def generate_document(self, extra_context: dict = None):
extra_context = extra_context or {}
context = self.get_document_context()
context.update(extra_context)
with open(self.template.file.path, "r", encoding="utf-8") as f:
content = f.read()
template = Template(content)
context = self.get_document_context()
content = template.render(context)
return pdfkit.from_string(content, False)

Wyświetl plik

@ -1,6 +1,9 @@
from rest_framework import serializers
from store.models import Product
from store.models import (
Product,
ProductAuthor
)
class TagSerializer(serializers.Serializer):
@ -21,6 +24,17 @@ class CartProductSerializer(serializers.Serializer):
quantity = serializers.IntegerField()
class ProductAuthorSerializer(serializers.Serializer):
class Meta:
model = ProductAuthor
fields = ["display_name"]
class CartSerializer(serializers.Serializer):
author = ProductAuthorSerializer()
products = CartProductSerializer(many=True)
class CartProductAddSerializer(serializers.Serializer):
product_id = serializers.IntegerField()

Wyświetl plik

@ -133,7 +133,9 @@ $(document).on('click', '.add-to-cart-button', function(event) {
data: formData, // Use the serialized form data
headers: { 'X-CSRFToken': csrfToken },
dataType: 'json',
success: location.reload(),
success: function(data) {
setTimeout(location.reload(), 500)
},
processData: false, // Prevent jQuery from processing the data
contentType: false, // Let the browser set the content type
});

Wyświetl plik

@ -9,15 +9,20 @@
<h3 class="fw-normal mb-0 text-black">Koszyk</h3>
</div>
</div>
{% for item in cart.get_items %}
{% include 'store/partials/cart_item.html' %}
{% for group in cart.get_items %}
{% if group.products %}
<h4>Wykonawca: {{group.author.display_name}}</h4>
{% for item in group.products %}
{% include 'store/partials/cart_item.html' %}
{% endfor %}
{% endif %}
{% endfor %}
<div class="card ">
<div class="card-body">
<div class="row">
<div class="col-sm-6">
<h5 class="fw-normal mb-0 text-black">Do zapłaty: {{cart.total_price}}</h5>
<h5 class="fw-normal mb-0 text-black">W sumie do zapłaty: {{cart.total_price}}</h5>
</div>
<div class="col-sm-6 text-end">
<a href="{% url 'order' %}" class="btn btn-success btn-block btn-lg">Dalej</a>

Wyświetl plik

@ -14,7 +14,7 @@
<p class="mb-0">Imię i Nazwisko</p>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">{{customer_data.full_name}}</p>
<p class="text-muted mb-0">{{customer_data.name}} {{customer_data.surname}}</p>
</div>
</div>
<hr>
@ -41,7 +41,10 @@
<p class="mb-0">Adres</p>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">{{customer_data.full_address}}</p>
<p class="text-muted mb-0">
{{customer_data.city}}, {{customer_data.zip_code}}<br/>
{{customer_data.street}}
</p>
</div>
</div>
</div>
@ -54,8 +57,13 @@
<h3 class="fw-normal mb-0 text-black">Zamówione przedmioty</h3>
</div>
</div>
{% for item in cart.get_items %}
{% include 'store/partials/summary_cart_item.html' %}
{% for group in cart.get_items %}
{% if group.products %}
<h4>Wykonawca: {{group.author.display_name}}</h4>
{% for item in group.products %}
{% include 'store/partials/summary_cart_item.html' %}
{% endfor %}
{% endif %}
{% endfor %}
<div class="card ">

Wyświetl plik

@ -1,6 +1,6 @@
{% load static %}
<div class="card rounded-3 mb-4">
<div class="card rounded-3 mb-1">
<div class="card-body p-4">
<div class="row d-flex justify-content-between align-items-center">
<div class="col-md-2 col-lg-2 col-xl-2">

Wyświetl plik

@ -1,16 +1,17 @@
from factory import (
Faker,
SubFactory
SubFactory,
Factory
)
from factory.django import (
FileField,
DjangoModelFactory
DjangoModelFactory,
)
class CustomerDataFactory(DjangoModelFactory):
class ProductAuthorFactory(DjangoModelFactory):
class Meta:
model = 'store.CustomerData'
model = 'store.ProductAuthor'
name = Faker('name')
surname = Faker('name')
@ -20,13 +21,61 @@ class CustomerDataFactory(DjangoModelFactory):
city = Faker('city')
zip_code = Faker('postcode')
country = Faker('country')
display_name = Faker('name')
class ProductCategoryFactory(DjangoModelFactory):
class Meta:
model = 'store.ProductCategory'
name = Faker('name')
class ProductCategoryParamFactory(DjangoModelFactory):
class Meta:
model = 'store.ProductCategoryParam'
key = Faker('name')
category = SubFactory(ProductCategoryFactory)
param_type = 'str'
class ProductTemplateFactory(DjangoModelFactory):
class Meta:
model = 'store.ProductTemplate'
title = Faker('name')
description = Faker('text')
code = Faker('name')
author = SubFactory(ProductAuthorFactory)
category = SubFactory(ProductCategoryFactory)
class ProductFactory(DjangoModelFactory):
class Meta:
model = 'store.Product'
name = Faker('name')
info = Faker('text')
price = Faker('pydecimal', left_digits=5, right_digits=2, positive=True)
available = Faker('boolean')
template = SubFactory(ProductTemplateFactory)
class PaymentMethodFactory(DjangoModelFactory):
class Meta:
model = 'store.PaymentMethod'
name = Faker('name')
description = Faker('text')
active = Faker('boolean')
class OrderFactory(DjangoModelFactory):
class Meta:
model = 'store.Order'
customer = SubFactory(CustomerDataFactory)
payment_method = SubFactory(PaymentMethodFactory)
created_at = Faker('date_time')
updated_at = Faker('date_time')
sent = Faker('boolean')
@ -38,4 +87,4 @@ class DocumentTemplateFactory(DjangoModelFactory):
name = Faker('name')
file = FileField(filename="doc.odt")
doc_type = "AGREEMENT"
doc_type = "agreement"

Wyświetl plik

@ -0,0 +1,120 @@
from rest_framework.test import APITestCase
from django.urls import reverse
from django.conf import settings
from store.tests import factories
class SessionCartTestCase(APITestCase):
def setUp(self):
super().setUp()
self.product = factories.ProductFactory(price=100)
self.second_product = factories.ProductFactory(price=200)
def test_add_item_simple_success(self):
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
1
)
def test_add_item_complex_success(self):
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
1
)
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
2
)
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.second_product.id, "quantity": 5},
)
final_dict = {
str(self.product.author.id): {
str(self.product.id): 2,
}
}
final_dict.update({
str(self.second_product.author.id): {
str(self.second_product.id): 5,
}
})
self.assertDictEqual(
self.client.session[settings.CART_SESSION_ID], final_dict
)
def test_add_item_invalid_product_id(self):
response = self.client.post(
reverse("cart-action-add-product"),
{"product_id": 999, "quantity": 1},
)
self.assertEqual(response.status_code, 400)
def test_add_item_invalid_quantity(self):
response = self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": "invalid"},
)
self.assertEqual(response.status_code, 400)
def test_remove_item_success(self):
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
1
)
self.client.post(
reverse("cart-action-remove-product"),
{"product_id": self.product.id},
)
self.assertEqual(self.client.session[settings.CART_SESSION_ID], {str(self.product.author.id): {}})
def test_remove_item_invalid_product_id(self):
response = self.client.post(
reverse("cart-action-remove-product"),
{"product_id": 999},
)
self.assertEqual(response.status_code, 400)
def test_update_item_quantity_success(self):
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
1
)
self.client.put(
reverse("cart-action-update-product", kwargs={"pk": self.product.id}),
{"quantity": 5},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
5
)
def test_update_item_quantity_invalid_product_id(self):
response = self.client.put(
reverse("cart-action-update-product", kwargs={"pk": 2137}),
{"quantity": 5},
)
self.assertEqual(response.status_code, 404)

Wyświetl plik

@ -1,5 +1,6 @@
from django.test import TestCase
from django.urls import reverse
from django.core import mail
from store.tests import factories
from store import models as store_models
@ -9,33 +10,102 @@ from store import models as store_models
# https://factoryboy.readthedocs.io/en/stable/
# TODO - test have to rewritten - I'll do it tommorow
class OrderDocumentTestCase(TestCase):
class OrderProductTestCase(TestCase):
def setUp(self):
super().setUp()
self.author = factories.ProductAuthorFactory()
self.order = factories.OrderFactory()
self.document_template = factories.DocumentTemplateFactory(file__data="test")
def test_generate_document_success(self):
order_doc = store_models.OrderDocument.objects.create(
order=self.order,
template=self.document_template
)
document = order_doc.document
self.assertIsInstance(document, bytes)
def test_get_document_context_success(self):
order_doc = store_models.OrderDocument.objects.create(
order=self.order,
template=self.document_template
)
context = order_doc.get_document_context()
self.assertIsInstance(context, store_models.Context)
self.assertEqual(context["order"].id, self.order.id)
self.assertEqual(context["customer"].id, self.order.customer.id)
self.assertEqual(context["products"].count(), 0)
self.product = factories.ProductFactory(template__author=self.author, price=100)
self.second_product = factories.ProductFactory(template__author=self.author, price=200)
def test_send_order_document_mail_success(self):
...
def test_send_order_document_mail_failure_wrong_email(self):
...
def test_create_from_cart_single_product_success(self):
products = store_models.OrderProduct.objects.create_from_cart(
items=[{"product": self.product, "quantity": 1}],
order=self.order
)
self.assertEqual(products.count(), 1)
def test_create_from_cart_multiple_products_success(self):
products = store_models.OrderProduct.objects.create_from_cart(
items=[
{"product": self.product, "quantity": 1},
{"product": self.second_product, "quantity": 1}
],
order=self.order
)
self.assertEqual(products.count(), 2)
def test_create_from_cart_wrong_quanitity_failure(self):
products = store_models.OrderProduct.objects.create_from_cart(
items=[{"product": self.product, "quantity": -123}],
order=self.order
)
self.assertEqual(products.count(), 0)
def test_create_from_cart_empty_data_failure(self):
products = store_models.OrderProduct.objects.create_from_cart(
items=[],
order=self.order
)
self.assertEqual(products.count(), 0)
class OrderTestCase(TestCase):
def setUp(self) -> None:
super().setUp()
self.author = factories.ProductAuthorFactory()
self.second_author = factories.ProductAuthorFactory()
self.customer_data = {
"first_name": "Jan",
"last_name": "Kowalski",
"email": "jan.kowalski@tepewu.pl",
"phone": "",
"address": "",
"postal_code": "",
"city": "",
"country": "",
}
self.payment_method = factories.PaymentMethodFactory()
factories.DocumentTemplateFactory()
factories.DocumentTemplateFactory(doc_type="receipt")
def test_create_from_cart_success_single_author(self):
product = factories.ProductFactory(template__author=self.author, price=100)
cart_items = [{
"author": self.author,
"products": [{"product": product, "quantity": 1}]
}]
orders = store_models.Order.objects.create_from_cart(
cart_items=cart_items,
customer_data=self.customer_data,
payment_method=self.payment_method
)
self.assertEqual(orders.count(), 1)
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[0].subject, f"Zamówienie {orders[0].order_number}")
def test_create_from_cart_success_multpile_authors(self):
product = factories.ProductFactory(template__author=self.second_author, price=100)
cart_items = [
{
"author": self.author,
"products": [{"product": product, "quantity": 1}]
}, {
"author": self.second_author,
"products": [{"product": product, "quantity": 1}]
}
]
orders = store_models.Order.objects.create_from_cart(
cart_items=cart_items,
customer_data=self.customer_data,
payment_method=self.payment_method
)
self.assertEqual(orders.count(), 2)
self.assertEqual(len(mail.outbox), 4)
self.assertEqual(mail.outbox[0].subject, f"Zamówienie {orders[0].order_number}")
self.assertEqual(mail.outbox[2].subject, f"Zamówienie {orders[1].order_number}")

Wyświetl plik

@ -1,18 +1,39 @@
from typing import Any
from django.core.mail import EmailMessage
from django.conf import settings
from django.db.models import QuerySet
# TODO - add celery task for sending not sent earlier
def send_mail(order_doc):
order = order_doc.order
def send_mail(
to: list[str], docs: Any, order_number: str,
subject: str, body: str
):
message = EmailMessage(
subject=f"Zamówienie {order.order_number}",
body="Dokumenty dla Twojego zamówienia",
subject=subject,
body=body,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[order.customer.email]
to=to
)
message.attach(f"{order.order_number}.pdf", order_doc.document, "application/pdf")
sent = bool(message.send())
order_doc.sent = sent
order_doc.save()
return sent
for doc in docs:
message.attach(f"{order_number}.pdf", doc, "application/pdf")
return bool(message.send())
def notify_user_about_order(customer_email, docs, order_number):
return send_mail(
to=[customer_email],
docs=docs,
order_number=order_number,
subject=f"Zamówienie {order_number}",
body="Dokumenty dla Twojego zamówienia"
)
def notify_manufacturer_about_order(manufacturer_email, docs, order_number):
return send_mail(
to=[manufacturer_email],
docs=docs,
order_number=order_number,
subject=f"Złożono zamówienie {order_number}",
body="Dokumenty dla złożonego zamówienia"
)

Wyświetl plik

@ -13,16 +13,13 @@ from rest_framework.response import Response
from store.cart import SessionCart
from store.serializers import (
CartProductSerializer,
CartSerializer,
CartProductAddSerializer
)
from store.forms import CustomerDataForm
from store.models import (
CustomerData,
Order,
OrderProduct,
OrderDocument,
DocumentTemplate
Product
)
@ -43,12 +40,13 @@ class CartView(TemplateView):
class CartActionView(ViewSet):
# TODO - test this, currently not in use
@action(detail=False, methods=["get"], url_path="list-products")
def list_products(self, request):
# get cart items
cart = SessionCart(self.request)
items = cart.get_items()
serializer = CartProductSerializer(instance=items, many=True)
serializer = CartSerializer(instance=items, many=True)
return Response(serializer.data)
@action(detail=False, methods=["post"])
@ -58,27 +56,32 @@ class CartActionView(ViewSet):
if not serializer.is_valid():
return Response(serializer.errors, status=400)
serializer.save(cart)
items = cart.get_items()
serializer = CartProductSerializer(instance=items, many=True)
serializer = CartSerializer(instance=items, many=True)
return Response(serializer.data, status=201)
@action(detail=False, methods=["post"])
def remove_product(self, request):
cart = SessionCart(self.request)
product_id = request.POST.get("product_id")
cart.remove_item(product_id)
try:
cart.remove_item(product_id)
except Product.DoesNotExist:
return Response({"error": "Product does not exist"}, status=400)
items = cart.get_items()
serializer = CartProductSerializer(instance=items, many=True)
serializer = CartSerializer(instance=items, many=True)
return Response(serializer.data, status=201)
@action(detail=True, methods=["put"])
def update_product(self, request, pk):
cart = SessionCart(self.request)
cart.update_item_quantity(pk, int(request.data["quantity"]))
try:
cart.update_item_quantity(pk, int(request.data["quantity"]))
except Product.DoesNotExist:
return Response({"error": "Product does not exist"}, status=404)
items = cart.get_items()
serializer = CartProductSerializer(instance=items, many=True)
serializer = CartSerializer(instance=items, many=True)
return Response(serializer.data, status=201)
@ -104,13 +107,12 @@ class OrderView(View):
return HttpResponseRedirect(reverse("cart"))
form = CustomerDataForm(request.POST)
if not form.is_valid():
print(form.errors)
context = self.get_context_data()
context["form"] = form
return render(request, self.template_name, context)
customer_data = form.save()
request.session["customer_data_id"] = customer_data.id
# TODO - add this page
customer_data = form.data
# TODO - add encryption
request.session["customer_data"] = customer_data
return HttpResponseRedirect(reverse("order-confirm"))
@ -118,7 +120,7 @@ class OrderConfirmView(View):
template_name = "store/order_confirm.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
customer_data = CustomerData.objects.get(id=self.request.session["customer_data_id"])
customer_data = self.request.session["customer_data"]
return {
"cart": SessionCart(self.request),
"customer_data": customer_data
@ -132,12 +134,13 @@ class OrderConfirmView(View):
return render(request, self.template_name, self.get_context_data())
def post(self, request):
customer_data = CustomerData.objects.get(id=self.request.session["customer_data_id"])
customer_data = request.session["customer_data"]
cart = SessionCart(self.request)
order = Order.objects.create_from_cart(
cart, customer_data
Order.objects.create_from_cart(
cart.get_items(),
None, customer_data
)
self.request.session.pop("customer_data_id")
request.session.pop("customer_data")
cart.clear()
# TODO - messages
return HttpResponseRedirect(reverse("cart"))