feat: added shop models
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import datetime
|
||||
from config.settings.environ import * # noqa
|
||||
|
||||
APP_NAME = "TPV"
|
||||
APP_NAME = "Shop"
|
||||
DESCRIPTION = ""
|
||||
VERSION = "0.1.0"
|
||||
|
||||
@@ -26,6 +26,7 @@ THIRD_PARTY_APPS = [
|
||||
]
|
||||
|
||||
PROJECT_APPS = [
|
||||
"shop",
|
||||
"tpv",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
-r requirements.txt
|
||||
pytest==7.3.0
|
||||
pytest-django==4.8.0
|
||||
coverage==7.2.3
|
||||
pytest-cov==4.0.0
|
||||
@@ -0,0 +1,51 @@
|
||||
from django.contrib import admin
|
||||
from shop.models import Product, ProductPrice, OrderLine, Order, Tax
|
||||
from unfold.admin import ModelAdmin
|
||||
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(ModelAdmin):
|
||||
search_fields = ("name",)
|
||||
list_display = (
|
||||
"id",
|
||||
"name",
|
||||
"stock",
|
||||
"unit",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(ProductPrice)
|
||||
class ProductPriceAdmin(ModelAdmin):
|
||||
search_fields = ("product",)
|
||||
autocomplete_fields = ("product",)
|
||||
list_display = (
|
||||
"id",
|
||||
"product",
|
||||
"price",
|
||||
"date",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Tax)
|
||||
class TaxAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"code",
|
||||
"value",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(OrderLine)
|
||||
class OrderLineAdmin(ModelAdmin):
|
||||
autocomplete_fields = ("product",)
|
||||
list_display = (
|
||||
"id",
|
||||
"product",
|
||||
"price",
|
||||
"quantity",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(ModelAdmin):
|
||||
list_display = ("id",)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ShopConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "shop"
|
||||
@@ -0,0 +1,250 @@
|
||||
# Generated by Django 5.0.3 on 2024-03-24 12:51
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from decimal import Decimal
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="OrderLine",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"quantity",
|
||||
models.DecimalField(
|
||||
decimal_places=4,
|
||||
default=Decimal("1"),
|
||||
max_digits=13,
|
||||
verbose_name="Cantidad",
|
||||
),
|
||||
),
|
||||
(
|
||||
"price",
|
||||
models.DecimalField(
|
||||
decimal_places=4, max_digits=13, verbose_name="Precio"
|
||||
),
|
||||
),
|
||||
(
|
||||
"base_total",
|
||||
models.DecimalField(
|
||||
decimal_places=4,
|
||||
default=Decimal("0"),
|
||||
max_digits=13,
|
||||
verbose_name="Total sin impuestos",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tax_value",
|
||||
models.PositiveIntegerField(verbose_name="Valor de impuestos"),
|
||||
),
|
||||
(
|
||||
"taxes",
|
||||
models.DecimalField(
|
||||
decimal_places=4, max_digits=13, verbose_name="Impuestos"
|
||||
),
|
||||
),
|
||||
(
|
||||
"total",
|
||||
models.DecimalField(
|
||||
decimal_places=4, max_digits=13, verbose_name="Total"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Línea de pedido",
|
||||
"verbose_name_plural": "Líneas de pedido",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Product",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=96, verbose_name="Nombre")),
|
||||
(
|
||||
"description",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
max_length=1000,
|
||||
verbose_name="Descripción",
|
||||
),
|
||||
),
|
||||
(
|
||||
"stock",
|
||||
models.DecimalField(
|
||||
decimal_places=4,
|
||||
default=Decimal("0"),
|
||||
max_digits=13,
|
||||
verbose_name="Stock",
|
||||
),
|
||||
),
|
||||
(
|
||||
"unit",
|
||||
models.CharField(
|
||||
choices=[("UNIT", "Unidad"), ("KG", "kg"), ("L", "L")],
|
||||
default="UNIT",
|
||||
max_length=6,
|
||||
verbose_name="Unidad de medida",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Producto",
|
||||
"verbose_name_plural": "Productos",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Tax",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"code",
|
||||
models.CharField(
|
||||
max_length=8, unique=True, verbose_name="Código de impuesto"
|
||||
),
|
||||
),
|
||||
(
|
||||
"value",
|
||||
models.PositiveIntegerField(
|
||||
verbose_name="Valor entero (porcentaje)"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Impuesto",
|
||||
"verbose_name_plural": "Impuestos",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Order",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Fecha de creación"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_modification_date",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Fecha de última modificación"
|
||||
),
|
||||
),
|
||||
(
|
||||
"base_total",
|
||||
models.DecimalField(
|
||||
decimal_places=2,
|
||||
default=Decimal("0"),
|
||||
max_digits=13,
|
||||
verbose_name="Base imponible",
|
||||
),
|
||||
),
|
||||
(
|
||||
"total",
|
||||
models.DecimalField(
|
||||
decimal_places=2,
|
||||
default=Decimal("0"),
|
||||
max_digits=13,
|
||||
verbose_name="Total",
|
||||
),
|
||||
),
|
||||
(
|
||||
"lines",
|
||||
models.ManyToManyField(to="shop.orderline", verbose_name="Líneas"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Pedido",
|
||||
"verbose_name_plural": "Pedidos",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="orderline",
|
||||
name="product",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="shop.product",
|
||||
verbose_name="Producto",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ProductPrice",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"price",
|
||||
models.DecimalField(
|
||||
decimal_places=2, max_digits=11, verbose_name="Precio"
|
||||
),
|
||||
),
|
||||
(
|
||||
"date",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="Fecha"
|
||||
),
|
||||
),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="prices",
|
||||
to="shop.product",
|
||||
verbose_name="Producto",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Precio de producto",
|
||||
"verbose_name_plural": "Precio de producto",
|
||||
},
|
||||
),
|
||||
]
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.text import gettext_lazy as _
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
class UnitChoices(models.TextChoices):
|
||||
UNIT = "UNIT", _("Unidad")
|
||||
WEIGHT_KG = "KG", _("kg")
|
||||
VOLUME_LITER = "L", _("L")
|
||||
|
||||
name = models.CharField(
|
||||
max_length=96,
|
||||
blank=False,
|
||||
null=False,
|
||||
verbose_name=_("Nombre"),
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=1000,
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("Descripción"),
|
||||
)
|
||||
stock = models.DecimalField(
|
||||
max_digits=13,
|
||||
decimal_places=4,
|
||||
default=Decimal("0"),
|
||||
verbose_name=_("Stock"),
|
||||
)
|
||||
unit = models.CharField(
|
||||
max_length=6,
|
||||
choices=UnitChoices.choices,
|
||||
default=UnitChoices.UNIT,
|
||||
verbose_name=_("Unidad de medida"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Producto")
|
||||
verbose_name_plural = _("Productos")
|
||||
|
||||
|
||||
class ProductPrice(models.Model):
|
||||
price = models.DecimalField(
|
||||
max_digits=11, decimal_places=2, verbose_name=_("Precio")
|
||||
)
|
||||
date = models.DateTimeField(default=timezone.now, verbose_name=_("Fecha"))
|
||||
product = models.ForeignKey(
|
||||
"shop.Product",
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("Producto"),
|
||||
related_name="prices",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.price}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Precio de producto")
|
||||
verbose_name_plural = _("Precio de producto")
|
||||
|
||||
|
||||
class Tax(models.Model):
|
||||
code = models.CharField(
|
||||
max_length=8, blank=False, unique=True, verbose_name=_("Código de impuesto")
|
||||
)
|
||||
value = models.PositiveIntegerField(
|
||||
null=False, blank=False, verbose_name=_("Valor entero (porcentaje)")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} - {self.value}%"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Impuesto")
|
||||
verbose_name_plural = _("Impuestos")
|
||||
|
||||
|
||||
class OrderLine(models.Model):
|
||||
product = models.ForeignKey(
|
||||
"shop.Product",
|
||||
on_delete=models.CASCADE,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_("Producto"),
|
||||
)
|
||||
quantity = models.DecimalField(
|
||||
max_digits=13,
|
||||
decimal_places=4,
|
||||
default=Decimal("1"),
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_("Cantidad"),
|
||||
)
|
||||
|
||||
# Precio del producto en el momento en el que se crea el pedido
|
||||
price = models.DecimalField(
|
||||
max_digits=13, decimal_places=4, verbose_name=_("Precio")
|
||||
)
|
||||
|
||||
# base_total = quantity * price
|
||||
base_total = models.DecimalField(
|
||||
max_digits=13,
|
||||
decimal_places=4,
|
||||
default=Decimal("0"),
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_("Total sin impuestos"),
|
||||
)
|
||||
|
||||
# Valor entero del impuesto en el momento en el que se crea el pedido
|
||||
tax_value = models.PositiveIntegerField(
|
||||
null=False, blank=False, verbose_name=_("Valor de impuestos")
|
||||
)
|
||||
|
||||
# taxes = price * quantity * (tax_value / 100)
|
||||
taxes = models.DecimalField(
|
||||
max_digits=13, decimal_places=4, verbose_name=_("Impuestos")
|
||||
)
|
||||
|
||||
# total = base_total + taxes
|
||||
total = models.DecimalField(
|
||||
max_digits=13, decimal_places=4, verbose_name=_("Total")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.name} - {self.quantity} - {self.price}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Línea de pedido")
|
||||
verbose_name_plural = _("Líneas de pedido")
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
lines = models.ManyToManyField("shop.OrderLine", verbose_name=_("Líneas"))
|
||||
creation_date = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name=_("Fecha de creación")
|
||||
)
|
||||
last_modification_date = models.DateTimeField(
|
||||
auto_now=True, verbose_name=_("Fecha de última modificación")
|
||||
)
|
||||
|
||||
base_total = models.DecimalField(
|
||||
default=Decimal("0"),
|
||||
max_digits=13,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Base imponible"),
|
||||
)
|
||||
total = models.DecimalField(
|
||||
default=Decimal("0"), max_digits=13, decimal_places=2, verbose_name=_("Total")
|
||||
)
|
||||
|
||||
def calculate_total_from_lines(self):
|
||||
self.base_total = self.lines.aggregate(base_total=models.Sum("base_total")).get(
|
||||
"base_total"
|
||||
)
|
||||
self.total = self.lines.aggregate(base_total=models.Sum("total")).get(
|
||||
"base_total"
|
||||
)
|
||||
|
||||
self.save()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Pedido")
|
||||
verbose_name_plural = _("Pedidos")
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,56 @@
|
||||
from decimal import Decimal
|
||||
from rest_framework.test import APITestCase as TestCase
|
||||
from shop.models import Product, ProductPrice, OrderLine, Tax
|
||||
from shop.utils import create_order_line_for_product, create_order
|
||||
|
||||
|
||||
class ShopModelsTest(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tax = Tax.objects.create(
|
||||
code="IVA",
|
||||
value=21,
|
||||
)
|
||||
self.create_products()
|
||||
|
||||
def create_products(self):
|
||||
self.potatoes = Product.objects.create(
|
||||
name="Patatas",
|
||||
stock=Decimal("100.00"),
|
||||
unit=Product.UnitChoices.WEIGHT_KG,
|
||||
)
|
||||
self.gasoline = Product.objects.create(
|
||||
name="Gasolina",
|
||||
stock=Decimal("800.00"),
|
||||
unit=Product.UnitChoices.VOLUME_LITER,
|
||||
)
|
||||
self.usb_c = Product.objects.create(
|
||||
name="Cable USB-C",
|
||||
stock=Decimal("5.00"),
|
||||
unit=Product.UnitChoices.UNIT,
|
||||
)
|
||||
|
||||
ProductPrice.objects.create(product=self.potatoes, price=Decimal("0.80"))
|
||||
ProductPrice.objects.create(product=self.gasoline, price=Decimal("1.15"))
|
||||
ProductPrice.objects.create(product=self.usb_c, price=Decimal("9.95"))
|
||||
|
||||
def test_create_order(self):
|
||||
l1 = create_order_line_for_product(
|
||||
self.potatoes,
|
||||
quantity=Decimal("1.5"),
|
||||
tax=self.tax,
|
||||
)
|
||||
l2 = create_order_line_for_product(
|
||||
self.gasoline,
|
||||
quantity=Decimal("40"),
|
||||
tax=self.tax,
|
||||
)
|
||||
l3 = create_order_line_for_product(
|
||||
self.usb_c,
|
||||
quantity=Decimal("1.00"),
|
||||
tax=self.tax,
|
||||
)
|
||||
|
||||
order = create_order(OrderLine.objects.all())
|
||||
|
||||
assert order.total == l1.total + l2.total + l3.total
|
||||
assert order.base_total == l1.base_total + l2.base_total + l3.base_total
|
||||
@@ -0,0 +1,29 @@
|
||||
from decimal import Decimal
|
||||
from shop.models import OrderLine, Tax, Product, Order
|
||||
from django.db.models import QuerySet
|
||||
|
||||
|
||||
def create_order_line_for_product(product: Product, quantity: Decimal, tax: Tax):
|
||||
price = product.prices.last().price
|
||||
base_total = round(price * quantity, 2)
|
||||
taxes = round(base_total * (tax.value / Decimal("100")), 2)
|
||||
|
||||
return OrderLine.objects.create(
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
price=price,
|
||||
base_total=base_total,
|
||||
tax_value=tax.value,
|
||||
taxes=taxes,
|
||||
total=base_total + taxes,
|
||||
)
|
||||
|
||||
|
||||
def create_order(lines: QuerySet):
|
||||
order = Order.objects.create()
|
||||
|
||||
for line in lines.all():
|
||||
order.lines.add(line)
|
||||
|
||||
order.calculate_total_from_lines()
|
||||
return order
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user