Merge pull request #13 from mtyton/feature/document_generator

Feature/document generator
pull/2/head
mtyton 2023-06-08 12:17:47 +02:00 zatwierdzone przez GitHub
commit d68f3087ef
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
27 zmienionych plików z 359 dodań i 48 usunięć

Wyświetl plik

@ -22,6 +22,7 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r
libjpeg62-turbo-dev \
zlib1g-dev \
libwebp-dev \
wkhtmltopdf \
&& rm -rf /var/lib/apt/lists/*
# Install the application server.

Wyświetl plik

@ -19,6 +19,7 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r
libjpeg62-turbo-dev \
zlib1g-dev \
libwebp-dev \
wkhtmltopdf \
&& rm -rf /var/lib/apt/lists/*
# Install the project requirements.

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -92,7 +92,6 @@ WSGI_APPLICATION = "artel.wsgi.application"
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
import dj_database_url as db_url
DATABASES = {
"default": db_url.parse(
os.environ.get("DATABASE_URL", "postgres://comfy:password@db/comfy_shop")
@ -171,10 +170,18 @@ WAGTAILSEARCH_BACKENDS = {
# Base URL to use when referring to full URLs within the Wagtail admin backend -
# e.g. in notification emails. Don't include '/admin' or a trailing slash
WAGTAILADMIN_BASE_URL = "http://example.com"
WAGTAILADMIN_BASE_URL = "https://artel.tepewu.pl"
# STORE SETTINGS
PRODUCTS_PER_PAGE = 6
# CART settings
CART_SESSION_ID = 'cart'
# EMAIL settings
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
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)
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'mtyton@tepewu.pl')

Wyświetl plik

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

Wyświetl plik

@ -0,0 +1,6 @@
from django.apps import AppConfig
class DocgeneratorConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'docgenerator'

Wyświetl plik

@ -0,0 +1,44 @@
from abc import (
ABC,
abstractmethod
)
from typing import (
Dict,
Any
)
from django.db.models import Model
from docxtpl import DocxTemplate
class DocumentGeneratorInterface(ABC):
@abstractmethod
def load_template(self, path: str):
...
@abstractmethod
def get_extra_context(self) -> Dict[str, Any]:
...
@abstractmethod
def generate_file(self, context: Dict[str, Any] = None):
...
class BaseDocumentGenerator(DocumentGeneratorInterface):
def __init__(self, instance: Model) -> None:
super().__init__()
self.instance = instance
def load_template(self, path: str):
return DocxTemplate(path)
def get_extra_context(self):
return {}
class PdfFromDocGenerator(BaseDocumentGenerator):
def generate_file(self, context: Dict[str, Any] = None):
template = self.load_template()
context.update(self.get_extra_context())

Wyświetl plik

@ -0,0 +1,10 @@
from django.db import models
from django.core.files.storage import Storage
class DocumentTemplate(models.Model):
name = models.CharField(max_length=255)
file = models.FileField(upload_to="doc_templates/", )
def __str__(self) -> str:
return self.name

Wyświetl plik

@ -0,0 +1,5 @@
from django.test import TestCase
class PdfFromDocGeneratorTestCase(TestCase):
...

Wyświetl plik

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

Wyświetl plik

@ -6,3 +6,5 @@ dj-database-url<=2.0.0
djangorestframework==3.14.0
phonenumbers==8.13.13
django-phonenumber-field==7.1.0
factory-boy==3.2.1
pdfkit==1.0.0

Wyświetl plik

@ -35,6 +35,12 @@ class ProductAdmin(ModelAdmin):
list_display = ("title", "price")
class DocumentTemplateAdmin(ModelAdmin):
model = models.DocumentTemplate
list_display = ("name", "doc_type")
class StoreAdminGroup(ModelAdminGroup):
menu_label = "Store"
menu_icon = 'folder-open-inverse'
@ -44,7 +50,8 @@ class StoreAdminGroup(ModelAdminGroup):
ProductCategoryAdmin,
ProductCategoryParamAdmin,
ProductTemplateAdmin,
ProductAdmin
ProductAdmin,
DocumentTemplateAdmin
)

Wyświetl plik

@ -0,0 +1,41 @@
# 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,22 @@
# 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,3 +1,4 @@
import pdfkit
from django.db import models
from django.core.paginator import (
Paginator,
@ -5,6 +6,10 @@ from django.core.paginator import (
)
from django.conf import settings
from django.core.validators import MinValueValidator
from django.template import (
Template,
Context
)
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
@ -17,6 +22,10 @@ from wagtail import fields as wagtail_fields
from taggit.managers import TaggableManager
from phonenumber_field.modelfields import PhoneNumberField
from store.utils import (
send_mail
)
class ProductAuthor(models.Model):
name = models.CharField(max_length=255)
@ -178,7 +187,7 @@ class CustomerData(models.Model):
class OrderProductManager(models.Manager):
def create_from_cart(self, cart, order):
for item in cart:
for item in cart.get_items():
self.create(
product=item.product,
order=order,
@ -194,8 +203,76 @@ class OrderProduct(models.Model):
objects = OrderProductManager()
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
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
class Order(models.Model):
customer = models.ForeignKey(CustomerData, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
sent = models.BooleanField(default=False)
objects = OrderManager()
@property
def order_number(self) -> str:
return f"{self.id:06}/{self.created_at.year}"
class DocumentTypeChoices(models.TextChoices):
AGREEMENT = "agreement"
RECEIPT = "receipt"
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)
created_at = models.DateTimeField(auto_now_add=True, null=True)
def __str__(self):
return self.name
class OrderDocument(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="documents")
template = models.ForeignKey(DocumentTemplate, on_delete=models.CASCADE)
sent = models.BooleanField(default=False)
def get_document_context(self):
_context = {
"order": self.order,
"customer": self.order.customer,
"products": self.order.products.all(),
}
return Context(_context)
@property
def document(self):
with open(self.template.file.path, "rb") 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

@ -65,7 +65,10 @@
<h5 class="fw-normal mb-0 text-black">To Pay: {{cart.total_price}}</h5>
</div>
<div class="col-sm-6 text-end">
<a href="" class="btn btn-success btn-block btn-lg">Confirm</a>
<form action="" method="POST">
{% csrf_token %}
<input type="submit" class="btn btn-success btn-block btn-lg" value="Confirm">
</form>
</div>
</div>

Wyświetl plik

@ -1,36 +0,0 @@
from django.test import TestCase
from django.urls import reverse
from store.models import ProductAuthor, ProductCategory, ProductTemplate, Product
# TODO - this is fine for now, but we'll want to use factoryboy for this:
# https://factoryboy.readthedocs.io/en/stable/
# TODO - test have to rewritten - I'll do it tommorow
class CartTestCase(TestCase):
def setUp(self):
self.productid = 1
self.author = ProductAuthor.objects.create(name='Test Author')
self.category = ProductCategory.objects.create(name='Test Category')
self.template = ProductTemplate.objects.create(category=self.category,
author=self.author,
title='Test title',
code='Test code',
description='Test description'
)
self.product = Product.objects.create(template=self.template,
price=10.99)
self.cart_url = reverse('view_cart')
def test_add_to_cart(self):
response = self.client.post(reverse('add_to_cart',
args=[self.productid]))
self.assertEqual(response.status_code, 302)
def test_remove_from_cart(self):
response = self.client.post(reverse('remove_from_cart',
args=[self.productid]))
self.assertEqual(response.status_code, 302)
def test_view_cart(self):
response = self.client.get(self.cart_url)
self.assertEqual(response.status_code, 200)

Wyświetl plik

@ -0,0 +1,41 @@
from factory import (
Faker,
SubFactory
)
from factory.django import (
FileField,
DjangoModelFactory
)
class CustomerDataFactory(DjangoModelFactory):
class Meta:
model = 'store.CustomerData'
name = Faker('name')
surname = Faker('name')
email = Faker('email')
phone = Faker('phone_number')
street = Faker('street_address')
city = Faker('city')
zip_code = Faker('postcode')
country = Faker('country')
class OrderFactory(DjangoModelFactory):
class Meta:
model = 'store.Order'
customer = SubFactory(CustomerDataFactory)
created_at = Faker('date_time')
updated_at = Faker('date_time')
sent = Faker('boolean')
class DocumentTemplateFactory(DjangoModelFactory):
class Meta:
model = 'store.DocumentTemplate'
name = Faker('name')
file = FileField(filename="doc.odt")
doc_type = "AGREEMENT"

Wyświetl plik

@ -0,0 +1,41 @@
from django.test import TestCase
from django.urls import reverse
from store.tests import factories
from store import models as store_models
# TODO - this is fine for now, but we'll want to use factoryboy for this:
# https://factoryboy.readthedocs.io/en/stable/
# TODO - test have to rewritten - I'll do it tommorow
class OrderDocumentTestCase(TestCase):
def setUp(self):
super().setUp()
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)
def test_send_order_document_mail_success(self):
...
def test_send_order_document_mail_failure_wrong_email(self):
...

Wyświetl plik

@ -12,4 +12,5 @@ urlpatterns = [
path('cart/', store_views.CartView.as_view(), name='cart'),
path("order/", store_views.OrderView.as_view(), name="order"),
path("order/confirm/", store_views.OrderConfirmView.as_view(), name="order-confirm"),
path("send-mail/", store_views.SendMailView.as_view(), name="send-mail"),
] + router.urls

Wyświetl plik

@ -0,0 +1,18 @@
from django.core.mail import EmailMessage
from django.conf import settings
# TODO - add celery task for sending not sent earlier
def send_mail(order_doc):
order = order_doc.order
message = EmailMessage(
subject=f"Zamówienie {order.order_number}",
body="Dokumenty dla Twojego zamówienia",
from_email=settings.DEFAULT_FROM_EMAIL,
to=[order.customer.email]
)
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

Wyświetl plik

@ -20,7 +20,9 @@ from store.forms import CustomerDataForm
from store.models import (
CustomerData,
Order,
OrderProduct
OrderProduct,
OrderDocument,
DocumentTemplate
)
@ -133,12 +135,24 @@ class OrderConfirmView(View):
def post(self, request):
customer_data = CustomerData.objects.get(id=self.request.session["customer_data_id"])
cart = SessionCart(self.request)
order = Order.objects.create(
customer=customer_data,
order = Order.objects.create_from_cart(
cart, customer_data
)
OrderProduct.objects.create_from_cart(order, cart)
cart.clear()
self.request.session.pop("customer_data_id")
# TODO - to be tested
cart.clear()
# TODO - messages
return HttpResponseRedirect(reverse("cart"))
return HttpResponseRedirect(reverse("cart"))
class SendMailView(View):
def get(self, request):
from django.core import mail
from django.http import HttpResponse
from django.conf import settings
r = mail.send_mail(
subject=f"Test",
message="Dokumenty dla Twojego zamówienia",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=["mateusz.tyton99@gmail.com"]
)
return HttpResponse(f"Mail sent: {r}")