new: first commit !minor
This commit is contained in:
commit
5848134067
45 changed files with 2754 additions and 0 deletions
0
order/__init__.py
Normal file
0
order/__init__.py
Normal file
8
order/admin.py
Normal file
8
order/admin.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from order.models import SaleOrder, SaleOrderLine, Product
|
||||
|
||||
admin.site.register(SaleOrder)
|
||||
admin.site.register(SaleOrderLine)
|
||||
admin.site.register(Product)
|
||||
5
order/apps.py
Normal file
5
order/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrderConfig(AppConfig):
|
||||
name = "order"
|
||||
54
order/fixtures/order.json
Normal file
54
order/fixtures/order.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
[
|
||||
{
|
||||
"model": "django.contrib.auth.models.User",
|
||||
"pk": 100,
|
||||
"fields": {
|
||||
"username": "alovelace",
|
||||
"first_name": "Ada",
|
||||
"last_name": "Lovelace",
|
||||
"email": "alovelace@berni.ni",
|
||||
"password": "alovelace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "django.contrib.auth.models.User",
|
||||
"pk": 200,
|
||||
"fields": {
|
||||
"username": "ltorvalds",
|
||||
"first_name": "Linus",
|
||||
"last_name": "Torvalds",
|
||||
"email": "ltorvalds@berni.ni",
|
||||
"password": "ltorvalds"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "django.contrib.auth.models.User",
|
||||
"pk": 300,
|
||||
"fields": {
|
||||
"username": "dritchie",
|
||||
"first_name": "Dennis",
|
||||
"last_name": "Ritchie",
|
||||
"email": "dritchie@berni.ni",
|
||||
"password": "dritchie"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "django.contrib.auth.models.User",
|
||||
"pk": 400,
|
||||
"fields": {
|
||||
"username": "kthompson",
|
||||
"first_name": "Ken",
|
||||
"last_name": "Thompson",
|
||||
"email": "kthompson@berni.ni",
|
||||
"password": "kthompson"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "SaleOrder",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Ken's first sale order",
|
||||
"user": 400
|
||||
}
|
||||
}
|
||||
]
|
||||
78
order/migrations/0001_initial.py
Normal file
78
order/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Generated by Django 3.1.5 on 2021-01-19 21:42
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Product",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=200)),
|
||||
("unit_price", models.IntegerField(default=0)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SaleOrder",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=200)),
|
||||
(
|
||||
"state",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("quoation", "Quotation"),
|
||||
("sale", "Sold"),
|
||||
("cancel", "Cancelled"),
|
||||
],
|
||||
max_length=200,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SaleOrderLine",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=200)),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to="order.product",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
55
order/migrations/0002_auto_20210124_2304.py
Normal file
55
order/migrations/0002_auto_20210124_2304.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Generated by Django 3.1.5 on 2021-01-24 23:04
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('order', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='saleorder',
|
||||
name='state',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='sold_at',
|
||||
field=models.DateTimeField(default=None, max_length=200, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='sold_to',
|
||||
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorder',
|
||||
name='total',
|
||||
field=models.FloatField(default=0.0, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorderline',
|
||||
name='order',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='order.saleorder'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='saleorderline',
|
||||
name='quantity',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='saleorder',
|
||||
name='name',
|
||||
field=models.CharField(max_length=200, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='saleorderline',
|
||||
name='product',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='order.product'),
|
||||
),
|
||||
]
|
||||
0
order/migrations/__init__.py
Normal file
0
order/migrations/__init__.py
Normal file
50
order/models.py
Normal file
50
order/models.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from datetime import datetime
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class ReadonlyMeta:
|
||||
readonly = []
|
||||
|
||||
|
||||
class SaleOrder(BaseModel):
|
||||
class ReadonlyMeta:
|
||||
readonly = ["sold_to", "sold_at", "total"]
|
||||
|
||||
name = models.CharField(max_length=200, unique=True)
|
||||
sold_to = models.ForeignKey(User, on_delete=models.DO_NOTHING, default=None, null=True)
|
||||
total = models.FloatField(max_length=200, default=0.0)
|
||||
sold_at = models.DateTimeField(max_length=200, null=True, default=None)
|
||||
|
||||
@property
|
||||
def amount_total(self) -> float:
|
||||
return sum(
|
||||
map(
|
||||
lambda x: x.product.unit_price * x.quantity,
|
||||
self.saleorderline_set.all(),
|
||||
)
|
||||
)
|
||||
|
||||
def sell(self, user: User):
|
||||
if not self.sold_at:
|
||||
self.sold_at = datetime.now()
|
||||
self.sold_to = user
|
||||
self.total = self.amount_total
|
||||
self.save()
|
||||
return self
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
unit_price = models.IntegerField(default=0)
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
product = models.ForeignKey(Product, on_delete=models.DO_NOTHING, null=True)
|
||||
order = models.ForeignKey(SaleOrder, on_delete=models.CASCADE, null=True)
|
||||
quantity = models.IntegerField(default=1)
|
||||
21
order/serializers.py
Normal file
21
order/serializers.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from order.models import SaleOrder, SaleOrderLine, Product
|
||||
|
||||
|
||||
class SaleOrderSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = SaleOrder
|
||||
fields = ["id", "name", "sold_to", "sold_at"]
|
||||
|
||||
|
||||
class SaleOrderLineSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = SaleOrderLine
|
||||
fields = ["id", "name", "product", "order", "quantity"]
|
||||
|
||||
|
||||
class ProductSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ["id", "name", "unit_price"]
|
||||
107
order/tests.py
Normal file
107
order/tests.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, Client
|
||||
from rest_framework import status
|
||||
from rest_framework.utils import json
|
||||
|
||||
from order.models import SaleOrder, SaleOrderLine, Product
|
||||
from order.serializers import SaleOrderSerializer
|
||||
|
||||
|
||||
class OrderTests(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.product_1: Product = Product.objects.create(name="Product 1", unit_price=10.0)
|
||||
self.product_2: Product = Product.objects.create(name="Product 2", unit_price=20.0)
|
||||
self.product_3: Product = Product.objects.create(name="Product 3", unit_price=30.0)
|
||||
self.product_4: Product = Product.objects.create(name="Product 4", unit_price=40.0)
|
||||
|
||||
self.order_1: SaleOrder = SaleOrder.objects.create(name="Order 1")
|
||||
self.order_2: SaleOrder = SaleOrder.objects.create(name="Order 2")
|
||||
|
||||
self.line_1_1: SaleOrderLine = SaleOrderLine.objects.create(
|
||||
name="Line 1/Order1", order=self.order_1, product=self.product_1
|
||||
)
|
||||
self.line_1_2: SaleOrderLine = SaleOrderLine.objects.create(
|
||||
name="Line 2/Order 1", quantity=3, order=self.order_1, product=self.product_2
|
||||
)
|
||||
|
||||
self.line_2_1: SaleOrderLine = SaleOrderLine.objects.create(
|
||||
name="Line 1/Order2", order=self.order_2, product=self.product_3
|
||||
)
|
||||
|
||||
self.line_2_2: SaleOrderLine = SaleOrderLine.objects.create(
|
||||
name="Line 2/Order2", order=self.order_2, product=self.product_4
|
||||
)
|
||||
|
||||
self.user_foo: User = User.objects.create(
|
||||
username="foo"
|
||||
)
|
||||
self.user_foo.set_password("foo")
|
||||
self.user_foo.save()
|
||||
self.user_bar = User.objects.create(
|
||||
username="bar", password="bar"
|
||||
)
|
||||
|
||||
self.client = Client()
|
||||
self.client.login(username="foo", password="foo")
|
||||
|
||||
def test_model_getters(self):
|
||||
self.assertEqual(len(self.order_1.saleorderline_set.all()), 2)
|
||||
self.assertEqual(self.order_1.total, 0.0)
|
||||
self.assertEqual(self.order_1.amount_total, 70.0)
|
||||
self.assertEqual(len(self.order_2.saleorderline_set.all()), 2)
|
||||
self.assertEqual(self.order_2.total, 0.0)
|
||||
self.assertEqual(self.order_2.amount_total, 70.0)
|
||||
self.assertIsNotNone(self.line_1_1.product)
|
||||
self.assertIsNotNone(self.line_1_2.product)
|
||||
self.assertIsNotNone(self.line_2_1.product)
|
||||
self.assertIsNotNone(self.line_2_2.product)
|
||||
|
||||
def test_model_sell(self):
|
||||
self.assertIsNone(self.order_1.sold_at)
|
||||
self.assertIsNone(self.order_1.sold_to)
|
||||
self.order_1.sell(user=self.user_bar)
|
||||
old_date = self.order_1.sold_at
|
||||
old_user = self.order_1.sold_to
|
||||
self.assertIsNotNone(self.order_1.sold_at)
|
||||
self.assertIsNotNone(self.order_1.sold_to)
|
||||
self.assertEqual(self.order_1.total, self.order_1.amount_total)
|
||||
self.assertEqual(self.order_1.total, 70.0)
|
||||
self.order_1.sell(user=self.user_bar)
|
||||
self.assertEqual(self.order_1.sold_at, old_date)
|
||||
self.assertEqual(self.order_1.sold_to, old_user)
|
||||
|
||||
def test_view_readonly(self):
|
||||
response = self.client.patch(
|
||||
path="/api/orders/1/",
|
||||
data={
|
||||
"sold_at": "foo"
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
response = self.client.patch(
|
||||
path="/api/orders/1/",
|
||||
data={
|
||||
"sold_to": self.user_foo.id
|
||||
},
|
||||
content_type="application/json"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_view_total(self):
|
||||
response = self.client.get(
|
||||
path="/api/orders/1/total/"
|
||||
)
|
||||
self.assertEqual(json.loads(response.content.decode())['amount_total'], 70.0)
|
||||
response = self.client.get(
|
||||
path="/api/orders/2/total/"
|
||||
)
|
||||
self.assertEqual(json.loads(response.content.decode())['amount_total'], 70.0)
|
||||
|
||||
def test_view_sell(self):
|
||||
response = self.client.get(
|
||||
path="/api/orders/2/sold/"
|
||||
)
|
||||
self.order_2.refresh_from_db()
|
||||
self.assertEqual(self.order_2.sold_to, self.user_foo)
|
||||
113
order/views.py
Normal file
113
order/views.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
from django.core.mail import EmailMessage
|
||||
from django.shortcuts import render
|
||||
from rest_framework import viewsets, permissions, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from bernini import settings
|
||||
from order.models import SaleOrder, SaleOrderLine, Product
|
||||
from order.serializers import (
|
||||
SaleOrderSerializer,
|
||||
SaleOrderLineSerializer,
|
||||
ProductSerializer,
|
||||
)
|
||||
|
||||
|
||||
class SaleOrderViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows orders to be viewed or edited. A `SaleOrder` object will render the general sale
|
||||
document that wraps n products a client can buy, using an intermediate model called `SaleOrderLine`, this means:
|
||||
a `SaleOrder` object does not interact directly with a product, a `SaleOrderLine` is just a wrap of a product,
|
||||
which holds its price-per-unit value, and a quantity for that product; lastly, on the other hand, a `SaleOrder`
|
||||
object will hold the total information by doing a sum() operation with all the lines
|
||||
"""
|
||||
|
||||
queryset = SaleOrder.objects.all().order_by("name")
|
||||
serializer_class = SaleOrderSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def update(self, request, pk=None, *args, **kwargs):
|
||||
order: SaleOrder = self.get_object()
|
||||
|
||||
for field in SaleOrder.ReadonlyMeta.readonly:
|
||||
if request.data.get(field) != SaleOrderSerializer(order).data[field]:
|
||||
return Response(
|
||||
data=f"You can't manually set the field {field}",
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
return super(SaleOrderViewSet, self).update(request, *args, **kwargs)
|
||||
|
||||
def partial_update(self, request, pk=None, *args, **kwargs):
|
||||
order: SaleOrder = self.get_object()
|
||||
for field in SaleOrder.ReadonlyMeta.readonly:
|
||||
if request.data.get(field) != SaleOrderSerializer(order).data[field]:
|
||||
return Response(
|
||||
data=f"You can't manually set the field {field}",
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
return super(SaleOrderViewSet, self).partial_update(request, *args, **kwargs)
|
||||
|
||||
@action(detail=True, name="Get the total amount")
|
||||
def total(self, request, pk=None):
|
||||
order: SaleOrder = self.get_object()
|
||||
return Response({"amount_total": order.amount_total})
|
||||
|
||||
@action(detail=True)
|
||||
def sold(self, request, pk=None):
|
||||
"""
|
||||
Try to sell a `SaleOrder` object, this is, the sale will have assigned
|
||||
the user which is doing this operation, the persistent total attribute
|
||||
will be set -`total`- and the `sold_at`
|
||||
:param request:
|
||||
:param pk:
|
||||
:return: the object with -possibly- updated values
|
||||
"""
|
||||
return Response(
|
||||
SaleOrderSerializer(
|
||||
self.get_object().sell(request.user), context={"request": request}
|
||||
).data
|
||||
)
|
||||
|
||||
@action(detail=True)
|
||||
def sent(self, request, pk=None):
|
||||
order: SaleOrder = self.get_object()
|
||||
|
||||
if order.sold_at and order.sold_to.email and settings.EMAIL_HOST_USER:
|
||||
mail = EmailMessage(
|
||||
f'Order {order.name} from Zapatos Bernini',
|
||||
render(
|
||||
'order_sold.html',
|
||||
{
|
||||
'order': order
|
||||
},
|
||||
),
|
||||
settings.EMAIL_HOST_USER,
|
||||
[order.sold_to.email],
|
||||
reply_to=[settings.EMAIL_HOST_USER]
|
||||
)
|
||||
mail.send()
|
||||
return Response(status=status.HTTP_200_OK,
|
||||
data=f"Email sent! You should receive it at {order.sold_to.email}")
|
||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data="Something went wrong on our end")
|
||||
|
||||
|
||||
class SaleOrderLineViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows orders to be viewed or edited.
|
||||
"""
|
||||
|
||||
queryset = SaleOrderLine.objects.all().order_by("name")
|
||||
serializer_class = SaleOrderLineSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
API endpoint that allows orders to be viewed or edited
|
||||
"""
|
||||
|
||||
queryset = Product.objects.all().order_by("name")
|
||||
serializer_class = ProductSerializer
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
Loading…
Add table
Add a link
Reference in a new issue