feat: add CatalogSale model with abstract base classes for Sale/SaleLine
- Introduced SaleAbstractModel and SaleLineAbstractModel as abstract bases - Added CatalogSale and CatalogSaleLine models inheriting from them - Created migration 0045 for new models - Added CatalogSaleView, CatalogSaleSerializer with nested line creation - Registered new models in admin - Added catalog_sales router endpoint to URLs - Removed placeholder api/ package (now redundant)
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
from django.contrib import admin
|
||||
from .models.sales import Sale, SaleLine
|
||||
from .models.customers import Customer
|
||||
from .models.sales import Sale, SaleLine, Payment
|
||||
from .models.sales import (
|
||||
Sale,
|
||||
SaleLine,
|
||||
CatalogSale,
|
||||
CatalogSaleLine,
|
||||
Payment,
|
||||
)
|
||||
from .models.products import Product, ProductCategory
|
||||
from .models.payments import ReconciliationJar
|
||||
|
||||
admin.site.register(Customer)
|
||||
admin.site.register(Sale)
|
||||
admin.site.register(SaleLine)
|
||||
admin.site.register(CatalogSale)
|
||||
admin.site.register(CatalogSaleLine)
|
||||
admin.site.register(Product)
|
||||
admin.site.register(ProductCategory)
|
||||
admin.site.register(Payment)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from rest_framework import viewsets
|
||||
|
||||
|
||||
class CatalogSaleView(viewsets.ViewSet):
|
||||
pass
|
||||
@@ -7,13 +7,20 @@ from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from .models.sales import Sale, SaleLine
|
||||
from .models.customers import Customer
|
||||
from .models.sales import Sale, SaleLine, Payment
|
||||
from .models.sales import (
|
||||
Sale,
|
||||
SaleLine,
|
||||
CatalogSale,
|
||||
CatalogSaleLine,
|
||||
Payment,
|
||||
)
|
||||
from .models.products import Product, ProductCategory
|
||||
from .models.payments import PaymentMethods, ReconciliationJar
|
||||
from .models.admin import AdminCode
|
||||
|
||||
from .serializers import (
|
||||
SaleSerializer,
|
||||
CatalogSaleSerializer,
|
||||
ProductSerializer,
|
||||
CustomerSerializer,
|
||||
ReconciliationJarSerializer,
|
||||
@@ -79,6 +86,11 @@ class SaleView(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
class CatalogSaleView(viewsets.ModelViewSet):
|
||||
queryset = CatalogSale.objects.all()
|
||||
serializer_class = CatalogSaleSerializer
|
||||
|
||||
|
||||
class ProductView(viewsets.ModelViewSet):
|
||||
queryset = Product.objects.all()
|
||||
serializer_class = ProductSerializer
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 5.0.6 on 2026-05-28 21:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('don_confiao', '0044_alter_payment_type_payment_alter_sale_payment_method'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CatalogSale',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateTimeField(verbose_name='Date')),
|
||||
('phone', models.CharField(blank=True, max_length=13, null=True)),
|
||||
('description', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='don_confiao.customer')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CatalogSaleLine',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
|
||||
('unit_price', models.DecimalField(decimal_places=2, max_digits=9)),
|
||||
('description', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('catalog_sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='don_confiao.catalogsale')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='don_confiao.product')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -6,11 +6,31 @@ from django.core.exceptions import ValidationError
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Sale(models.Model):
|
||||
class SaleAbstractModel(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
|
||||
date = models.DateTimeField("Date")
|
||||
phone = models.CharField(max_length=13, null=True, blank=True)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class SaleLineAbstractModel(models.Model):
|
||||
product = models.ForeignKey(
|
||||
Product, null=False, blank=False, on_delete=models.CASCADE
|
||||
)
|
||||
quantity = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, null=True
|
||||
)
|
||||
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Sale(SaleAbstractModel):
|
||||
payment_method = models.CharField(
|
||||
max_length=30,
|
||||
choices=PaymentMethods.choices,
|
||||
@@ -46,22 +66,31 @@ class Sale(models.Model):
|
||||
return sale_header_csv
|
||||
|
||||
|
||||
class SaleLine(models.Model):
|
||||
class SaleLine(SaleLineAbstractModel):
|
||||
|
||||
sale = models.ForeignKey(Sale, on_delete=models.CASCADE)
|
||||
product = models.ForeignKey(
|
||||
Product, null=False, blank=False, on_delete=models.CASCADE
|
||||
)
|
||||
quantity = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, null=True
|
||||
)
|
||||
unit_price = models.DecimalField(max_digits=9, decimal_places=2)
|
||||
description = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.sale} - {self.product}"
|
||||
|
||||
|
||||
class CatalogSale(SaleAbstractModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.date} {self.customer}"
|
||||
|
||||
def get_total(self):
|
||||
lines = self.catalogsaleline_set.all()
|
||||
return sum([l.quantity * l.unit_price for l in lines])
|
||||
|
||||
|
||||
class CatalogSaleLine(SaleLineAbstractModel):
|
||||
catalog_sale = models.ForeignKey(CatalogSale, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.catalog_sale} - {self.product}"
|
||||
|
||||
|
||||
class Payment(models.Model):
|
||||
date_time = models.DateTimeField()
|
||||
type_payment = models.CharField(
|
||||
|
||||
@@ -2,7 +2,13 @@ from rest_framework import serializers
|
||||
|
||||
from .models.sales import Sale, SaleLine
|
||||
from .models.customers import Customer
|
||||
from .models.sales import Sale, SaleLine, Payment
|
||||
from .models.sales import (
|
||||
Sale,
|
||||
SaleLine,
|
||||
CatalogSale,
|
||||
CatalogSaleLine,
|
||||
Payment,
|
||||
)
|
||||
from .models.products import Product, ProductCategory
|
||||
from .models.payments import ReconciliationJar
|
||||
|
||||
@@ -29,6 +35,44 @@ class SaleSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class CatalogSaleLineSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CatalogSaleLine
|
||||
fields = [
|
||||
"id",
|
||||
"catalog_sale",
|
||||
"product",
|
||||
"unit_price",
|
||||
"quantity",
|
||||
]
|
||||
|
||||
|
||||
class CatalogSaleSerializer(serializers.ModelSerializer):
|
||||
catalogsaleline_set = CatalogSaleLineSerializer(
|
||||
many=True, required=False
|
||||
)
|
||||
total = serializers.ReadOnlyField(source="get_total")
|
||||
|
||||
class Meta:
|
||||
model = CatalogSale
|
||||
fields = [
|
||||
"id",
|
||||
"customer",
|
||||
"date",
|
||||
"catalogsaleline_set",
|
||||
"total",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
lines_data = validated_data.pop("catalogsaleline_set", [])
|
||||
catalog_sale = CatalogSale.objects.create(**validated_data)
|
||||
for line_data in lines_data:
|
||||
CatalogSaleLine.objects.create(
|
||||
catalog_sale=catalog_sale, **line_data
|
||||
)
|
||||
return catalog_sale
|
||||
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Product
|
||||
|
||||
@@ -34,6 +34,16 @@ class TestAPI(APITestCase, LoginMixin):
|
||||
self.assertEqual(sale.id, content["id"])
|
||||
self.assertIsNone(sale.external_id)
|
||||
|
||||
def test_create_catalog_sale(self):
|
||||
response = self._create_catalog_sale()
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
# self.assertEqual(Sale.objects.count(), 1)
|
||||
# sale = Sale.objects.all()[0]
|
||||
# self.assertEqual(sale.customer.name, self.customer.name)
|
||||
# self.assertEqual(sale.id, content["id"])
|
||||
# self.assertTrue(sale.catalog_sale)
|
||||
|
||||
def test_create_sale_with_decimal(self):
|
||||
response = self._create_sale_with_decimal()
|
||||
content = json.loads(response.content.decode("utf-8"))
|
||||
|
||||
@@ -8,6 +8,9 @@ app_name = "don_confiao"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"sales", api_views.SaleView, basename="sale")
|
||||
router.register(
|
||||
r"catalog_sales", api_views.CatalogSaleView, basename="catalog_sale"
|
||||
)
|
||||
router.register(r"customers", api_views.CustomerView, basename="customer")
|
||||
router.register(r"products", api_views.ProductView, basename="product")
|
||||
router.register(
|
||||
@@ -23,6 +26,17 @@ urlpatterns = [
|
||||
api_views.SaleSummary.as_view(),
|
||||
name="purchase_json_summary",
|
||||
),
|
||||
path(
|
||||
"payment_methods/all/select_format",
|
||||
api_views.PaymentMethodView.as_view(),
|
||||
name="payment_methods_to_select",
|
||||
),
|
||||
path(
|
||||
"purchases/for_reconciliation",
|
||||
api_views.SalesForReconciliationView.as_view(),
|
||||
name="sales_for_reconciliation",
|
||||
),
|
||||
path("reconciliate_jar", api_views.ReconciliateJarView.as_view()),
|
||||
path("api/", include(router.urls)),
|
||||
path(
|
||||
"api/importar_productos_de_tryton",
|
||||
@@ -39,17 +53,6 @@ urlpatterns = [
|
||||
api_views.SalesToTrytonView.as_view(),
|
||||
name="send_tryton",
|
||||
),
|
||||
path(
|
||||
"payment_methods/all/select_format",
|
||||
api_views.PaymentMethodView.as_view(),
|
||||
name="payment_methods_to_select",
|
||||
),
|
||||
path(
|
||||
"purchases/for_reconciliation",
|
||||
api_views.SalesForReconciliationView.as_view(),
|
||||
name="sales_for_reconciliation",
|
||||
),
|
||||
path("reconciliate_jar", api_views.ReconciliateJarView.as_view()),
|
||||
path(
|
||||
"api/admin_code/validate/<code>",
|
||||
api_views.AdminCodeValidateView.as_view(),
|
||||
|
||||
Reference in New Issue
Block a user