feat: added shop models
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from config.settings.environ import * # noqa
|
from config.settings.environ import * # noqa
|
||||||
|
|
||||||
APP_NAME = "TPV"
|
APP_NAME = "Shop"
|
||||||
DESCRIPTION = ""
|
DESCRIPTION = ""
|
||||||
VERSION = "0.1.0"
|
VERSION = "0.1.0"
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ THIRD_PARTY_APPS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
PROJECT_APPS = [
|
PROJECT_APPS = [
|
||||||
|
"shop",
|
||||||
"tpv",
|
"tpv",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
pytest==7.3.0
|
pytest==7.3.0
|
||||||
|
pytest-django==4.8.0
|
||||||
coverage==7.2.3
|
coverage==7.2.3
|
||||||
pytest-cov==4.0.0
|
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