Merge pull request #13 from mtyton/feature/document_generator
Feature/document generatorpull/2/head
commit
d68f3087ef
|
@ -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.
|
||||
|
|
|
@ -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.
|
@ -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')
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DocgeneratorConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'docgenerator'
|
|
@ -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())
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
from django.test import TestCase
|
||||
|
||||
|
||||
class PdfFromDocGeneratorTestCase(TestCase):
|
||||
...
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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"
|
|
@ -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):
|
||||
...
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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}")
|
||||
|
|
Ładowanie…
Reference in New Issue