feat: added product batches and provider models
This commit is contained in:
+1
-15
@@ -149,17 +149,6 @@ REST_FRAMEWORK = {
|
|||||||
"PAGE_SIZE": PAGE_SIZE,
|
"PAGE_SIZE": PAGE_SIZE,
|
||||||
}
|
}
|
||||||
|
|
||||||
JWT_AUTH = {
|
|
||||||
"JWT_SECRET_KEY": SECRET_KEY,
|
|
||||||
"JWT_VERIFY": True,
|
|
||||||
"JWT_VERIFY_EXPIRATION": True,
|
|
||||||
"JWT_EXPIRATION_DELTA": datetime.timedelta(days=7),
|
|
||||||
"JWT_ALLOW_REFRESH": True,
|
|
||||||
"JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=7),
|
|
||||||
"JWT_AUTH_HEADER_PREFIX": "JWT",
|
|
||||||
"JWT_AUTH_COOKIE": "jwt",
|
|
||||||
}
|
|
||||||
|
|
||||||
LOCALE_PATHS = [
|
LOCALE_PATHS = [
|
||||||
BASE_DIR / "locale",
|
BASE_DIR / "locale",
|
||||||
]
|
]
|
||||||
@@ -196,10 +185,7 @@ SIMPLE_JWT = {
|
|||||||
"JSON_ENCODER": None,
|
"JSON_ENCODER": None,
|
||||||
"JWK_URL": None,
|
"JWK_URL": None,
|
||||||
"LEEWAY": 0,
|
"LEEWAY": 0,
|
||||||
"AUTH_HEADER_TYPES": (
|
"AUTH_HEADER_TYPES": ("Bearer",),
|
||||||
"Bearer",
|
|
||||||
"JWT",
|
|
||||||
),
|
|
||||||
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
|
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
|
||||||
"USER_ID_FIELD": "id",
|
"USER_ID_FIELD": "id",
|
||||||
"USER_ID_CLAIM": "user_id",
|
"USER_ID_CLAIM": "user_id",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from shop.api.v1.viewsets import (
|
|||||||
ProductPriceViewSet,
|
ProductPriceViewSet,
|
||||||
OrderViewSet,
|
OrderViewSet,
|
||||||
OrderLineViewSet,
|
OrderLineViewSet,
|
||||||
|
ProviderViewSet,
|
||||||
|
ProductBatchViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -13,8 +15,10 @@ router = DefaultRouter()
|
|||||||
|
|
||||||
router.register("taxes", TaxViewSet)
|
router.register("taxes", TaxViewSet)
|
||||||
router.register("customers", CustomerViewSet)
|
router.register("customers", CustomerViewSet)
|
||||||
|
router.register("providers", ProviderViewSet)
|
||||||
router.register("products", ProductViewSet)
|
router.register("products", ProductViewSet)
|
||||||
router.register("product-prices", ProductPriceViewSet)
|
router.register("product-prices", ProductPriceViewSet)
|
||||||
|
router.register("product-batches", ProductBatchViewSet)
|
||||||
router.register("orders", OrderViewSet)
|
router.register("orders", OrderViewSet)
|
||||||
router.register("orders/(?P<order_id>[^/.]+)/lines", OrderLineViewSet)
|
router.register("orders/(?P<order_id>[^/.]+)/lines", OrderLineViewSet)
|
||||||
|
|
||||||
|
|||||||
+58
-21
@@ -1,27 +1,15 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from files.api.v1.serializers import FileUploadSerializer
|
from files.api.v1.serializers import FileUploadSerializer
|
||||||
from shop.models import Product, ProductPrice, Customer, Tax, OrderLine, Order
|
from shop.models import (
|
||||||
|
Product,
|
||||||
|
ProductPrice,
|
||||||
class ProductSerializer(serializers.ModelSerializer):
|
Customer,
|
||||||
images = FileUploadSerializer(many=True, read_only=True)
|
Tax,
|
||||||
price = serializers.SerializerMethodField()
|
OrderLine,
|
||||||
|
Order,
|
||||||
def get_price(self, product):
|
ProductBatch,
|
||||||
last_price = product.prices.last()
|
Provider,
|
||||||
|
|
||||||
return ProductPriceSerializer(last_price).data
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Product
|
|
||||||
fields = (
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"stock",
|
|
||||||
"is_digital_asset",
|
|
||||||
"images",
|
|
||||||
"price",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -47,6 +35,39 @@ class ProductPriceSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductSerializer(serializers.ModelSerializer):
|
||||||
|
images = FileUploadSerializer(many=True, read_only=True)
|
||||||
|
price = ProductPriceSerializer()
|
||||||
|
|
||||||
|
def get_price(self, product):
|
||||||
|
last_price = product.prices.last()
|
||||||
|
|
||||||
|
return ProductPriceSerializer(last_price).data
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = (
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"stock",
|
||||||
|
"is_digital_asset",
|
||||||
|
"images",
|
||||||
|
"price",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductBatchSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ProductBatch
|
||||||
|
fields = (
|
||||||
|
"code",
|
||||||
|
"product",
|
||||||
|
"quantity",
|
||||||
|
"creation_date",
|
||||||
|
"expiration_date",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomerSerializer(serializers.ModelSerializer):
|
class CustomerSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Customer
|
model = Customer
|
||||||
@@ -63,6 +84,22 @@ class CustomerSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Provider
|
||||||
|
fields = (
|
||||||
|
"vat_id",
|
||||||
|
"name",
|
||||||
|
"email",
|
||||||
|
"phone",
|
||||||
|
"address",
|
||||||
|
"city",
|
||||||
|
"state",
|
||||||
|
"country",
|
||||||
|
"zip",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OrderSerializer(serializers.ModelSerializer):
|
class OrderSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
|
|||||||
+22
-1
@@ -7,8 +7,19 @@ from shop.api.v1.serializers import (
|
|||||||
TaxSerializer,
|
TaxSerializer,
|
||||||
OrderSerializer,
|
OrderSerializer,
|
||||||
OrderLineSerializer,
|
OrderLineSerializer,
|
||||||
|
ProviderSerializer,
|
||||||
|
ProductBatchSerializer,
|
||||||
|
)
|
||||||
|
from shop.models import (
|
||||||
|
Product,
|
||||||
|
ProductPrice,
|
||||||
|
Customer,
|
||||||
|
Tax,
|
||||||
|
Order,
|
||||||
|
OrderLine,
|
||||||
|
Provider,
|
||||||
|
ProductBatch,
|
||||||
)
|
)
|
||||||
from shop.models import Product, ProductPrice, Customer, Tax, Order, OrderLine
|
|
||||||
|
|
||||||
|
|
||||||
class ProductViewSet(ModelViewSet):
|
class ProductViewSet(ModelViewSet):
|
||||||
@@ -21,11 +32,21 @@ class ProductPriceViewSet(ModelViewSet):
|
|||||||
queryset = ProductPrice.objects.all()
|
queryset = ProductPrice.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ProductBatchViewSet(ModelViewSet):
|
||||||
|
serializer_class = ProductBatchSerializer
|
||||||
|
queryset = ProductBatch.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class CustomerViewSet(ModelViewSet):
|
class CustomerViewSet(ModelViewSet):
|
||||||
serializer_class = CustomerSerializer
|
serializer_class = CustomerSerializer
|
||||||
queryset = Customer.objects.all()
|
queryset = Customer.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderViewSet(ModelViewSet):
|
||||||
|
serializer_class = ProviderSerializer
|
||||||
|
queryset = Provider.objects.all()
|
||||||
|
|
||||||
|
|
||||||
class TaxViewSet(ModelViewSet):
|
class TaxViewSet(ModelViewSet):
|
||||||
serializer_class = TaxSerializer
|
serializer_class = TaxSerializer
|
||||||
queryset = Tax.objects.all()
|
queryset = Tax.objects.all()
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-05-12 23:18
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("shop", "0002_remove_product_unit_order_status_product_images_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Provider",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"vat_id",
|
||||||
|
models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Documento de identidad",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=64, verbose_name="Nombre")),
|
||||||
|
(
|
||||||
|
"email",
|
||||||
|
models.EmailField(
|
||||||
|
max_length=254, verbose_name="E-mail de contacto"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"phone",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
max_length=16,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Teléfono de contacto",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("address", models.CharField(max_length=255, verbose_name="Dirección")),
|
||||||
|
("city", models.CharField(max_length=64, verbose_name="Ciudad")),
|
||||||
|
("state", models.CharField(max_length=64, verbose_name="Región")),
|
||||||
|
("country", models.CharField(max_length=64, verbose_name="País")),
|
||||||
|
("zip", models.CharField(max_length=32, verbose_name="Código postal")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Proveedor",
|
||||||
|
"verbose_name_plural": "Proveedores",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ProductBatch",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"code",
|
||||||
|
models.CharField(
|
||||||
|
db_index=True, max_length=32, unique=True, verbose_name="Código"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"quantity",
|
||||||
|
models.DecimalField(
|
||||||
|
decimal_places=4,
|
||||||
|
default=Decimal("1"),
|
||||||
|
max_digits=13,
|
||||||
|
verbose_name="Cantidad",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"creation_date",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="Fecha de creación"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"expiration_date",
|
||||||
|
models.DateField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Fecha de caducidad / consumo preferente",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"product",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="shop.product",
|
||||||
|
verbose_name="Producto",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
+57
-1
@@ -60,7 +60,7 @@ class ProductPrice(models.Model):
|
|||||||
return f"{self.price} - {self.tax.code}"
|
return f"{self.price} - {self.tax.code}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def price_with_tax(self):
|
def price_with_tax(self) -> Decimal:
|
||||||
tax_value = self.price * Decimal(self.tax.value / 100)
|
tax_value = self.price * Decimal(self.tax.value / 100)
|
||||||
return round(self.price + tax_value, 2)
|
return round(self.price + tax_value, 2)
|
||||||
|
|
||||||
@@ -69,6 +69,30 @@ class ProductPrice(models.Model):
|
|||||||
verbose_name_plural = _("Precio de producto")
|
verbose_name_plural = _("Precio de producto")
|
||||||
|
|
||||||
|
|
||||||
|
class ProductBatch(models.Model):
|
||||||
|
code = models.CharField(
|
||||||
|
max_length=32, unique=True, db_index=True, verbose_name=_("Código")
|
||||||
|
)
|
||||||
|
product = models.ForeignKey(
|
||||||
|
"shop.Product", on_delete=models.PROTECT, verbose_name=_("Producto")
|
||||||
|
)
|
||||||
|
quantity = models.DecimalField(
|
||||||
|
max_digits=13,
|
||||||
|
decimal_places=4,
|
||||||
|
default=Decimal("1"),
|
||||||
|
verbose_name=_("Cantidad"),
|
||||||
|
)
|
||||||
|
creation_date = models.DateTimeField(
|
||||||
|
auto_now_add=True, verbose_name=_("Fecha de creación")
|
||||||
|
)
|
||||||
|
expiration_date = models.DateField(
|
||||||
|
blank=True, null=True, verbose_name=_("Fecha de caducidad / consumo preferente")
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.code} - {self.product.name} - {self.quantity}"
|
||||||
|
|
||||||
|
|
||||||
class Tax(models.Model):
|
class Tax(models.Model):
|
||||||
code = models.CharField(
|
code = models.CharField(
|
||||||
max_length=8, blank=False, unique=True, verbose_name=_("Código de impuesto")
|
max_length=8, blank=False, unique=True, verbose_name=_("Código de impuesto")
|
||||||
@@ -112,6 +136,38 @@ class Customer(models.Model):
|
|||||||
verbose_name_plural = _("Clientes")
|
verbose_name_plural = _("Clientes")
|
||||||
|
|
||||||
|
|
||||||
|
class Provider(models.Model):
|
||||||
|
vat_id = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
blank=False,
|
||||||
|
unique=True,
|
||||||
|
verbose_name=_("Documento de identidad"),
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=64, blank=False, verbose_name=_("Nombre"))
|
||||||
|
|
||||||
|
email = models.EmailField(blank=False, verbose_name=_("E-mail de contacto"))
|
||||||
|
phone = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
default="",
|
||||||
|
verbose_name=_("Teléfono de contacto"),
|
||||||
|
)
|
||||||
|
|
||||||
|
address = models.CharField(max_length=255, blank=False, verbose_name=_("Dirección"))
|
||||||
|
city = models.CharField(max_length=64, blank=False, verbose_name=_("Ciudad"))
|
||||||
|
state = models.CharField(max_length=64, blank=False, verbose_name=_("Región"))
|
||||||
|
country = models.CharField(max_length=64, blank=False, verbose_name=_("País"))
|
||||||
|
zip = models.CharField(max_length=32, blank=False, verbose_name=_("Código postal"))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.vat_id} - {self.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Proveedor")
|
||||||
|
verbose_name_plural = _("Proveedores")
|
||||||
|
|
||||||
|
|
||||||
class OrderLine(models.Model):
|
class OrderLine(models.Model):
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
"shop.Order",
|
"shop.Order",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class TestChangePassword(APITestCase):
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
jwt_token = response.data.get("access")
|
jwt_token = response.data.get("access")
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}")
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}")
|
||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
"/api/v1/auth/change-password/",
|
"/api/v1/auth/change-password/",
|
||||||
{
|
{
|
||||||
@@ -50,7 +50,7 @@ class TestChangePassword(APITestCase):
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
jwt_token = response.data.get("access")
|
jwt_token = response.data.get("access")
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}")
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}")
|
||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
"/api/v1/auth/change-password/",
|
"/api/v1/auth/change-password/",
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,7 @@ class TestChangePassword(APITestCase):
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
jwt_token = response.data.get("access")
|
jwt_token = response.data.get("access")
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}")
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}")
|
||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
"/api/v1/auth/change-password/",
|
"/api/v1/auth/change-password/",
|
||||||
{
|
{
|
||||||
@@ -96,7 +96,7 @@ class TestChangePassword(APITestCase):
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
jwt_token = response.data.get("access")
|
jwt_token = response.data.get("access")
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}")
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}")
|
||||||
response = self.client.put(
|
response = self.client.put(
|
||||||
"/api/v1/auth/change-password/",
|
"/api/v1/auth/change-password/",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class TestLogin(APITestCase):
|
|||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
jwt_token = response.data.get("access")
|
jwt_token = response.data.get("access")
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"JWT {jwt_token}")
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {jwt_token}")
|
||||||
response = self.client.get("/api/v1/auth/me/")
|
response = self.client.get("/api/v1/auth/me/")
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|||||||
Reference in New Issue
Block a user