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:
2026-05-28 16:38:45 -05:00
parent f97b47081c
commit 47c18c760d
9 changed files with 171 additions and 29 deletions

View File

@@ -1,13 +1,21 @@
from django.contrib import admin from django.contrib import admin
from .models.sales import Sale, SaleLine from .models.sales import Sale, SaleLine
from .models.customers import Customer 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.products import Product, ProductCategory
from .models.payments import ReconciliationJar from .models.payments import ReconciliationJar
admin.site.register(Customer) admin.site.register(Customer)
admin.site.register(Sale) admin.site.register(Sale)
admin.site.register(SaleLine) admin.site.register(SaleLine)
admin.site.register(CatalogSale)
admin.site.register(CatalogSaleLine)
admin.site.register(Product) admin.site.register(Product)
admin.site.register(ProductCategory) admin.site.register(ProductCategory)
admin.site.register(Payment) admin.site.register(Payment)

View File

@@ -1,5 +0,0 @@
from rest_framework import viewsets
class CatalogSaleView(viewsets.ViewSet):
pass

View File

@@ -7,13 +7,20 @@ from rest_framework.permissions import IsAuthenticated
from .models.sales import Sale, SaleLine from .models.sales import Sale, SaleLine
from .models.customers import Customer 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.products import Product, ProductCategory
from .models.payments import PaymentMethods, ReconciliationJar from .models.payments import PaymentMethods, ReconciliationJar
from .models.admin import AdminCode from .models.admin import AdminCode
from .serializers import ( from .serializers import (
SaleSerializer, SaleSerializer,
CatalogSaleSerializer,
ProductSerializer, ProductSerializer,
CustomerSerializer, CustomerSerializer,
ReconciliationJarSerializer, ReconciliationJarSerializer,
@@ -79,6 +86,11 @@ class SaleView(viewsets.ModelViewSet):
) )
class CatalogSaleView(viewsets.ModelViewSet):
queryset = CatalogSale.objects.all()
serializer_class = CatalogSaleSerializer
class ProductView(viewsets.ModelViewSet): class ProductView(viewsets.ModelViewSet):
queryset = Product.objects.all() queryset = Product.objects.all()
serializer_class = ProductSerializer serializer_class = ProductSerializer

View File

@@ -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,
},
),
]

View File

@@ -6,11 +6,31 @@ from django.core.exceptions import ValidationError
from datetime import datetime from datetime import datetime
class Sale(models.Model): class SaleAbstractModel(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.PROTECT) customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
date = models.DateTimeField("Date") date = models.DateTimeField("Date")
phone = models.CharField(max_length=13, null=True, blank=True) phone = models.CharField(max_length=13, null=True, blank=True)
description = models.CharField(max_length=255, 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( payment_method = models.CharField(
max_length=30, max_length=30,
choices=PaymentMethods.choices, choices=PaymentMethods.choices,
@@ -46,22 +66,31 @@ class Sale(models.Model):
return sale_header_csv return sale_header_csv
class SaleLine(models.Model): class SaleLine(SaleLineAbstractModel):
sale = models.ForeignKey(Sale, on_delete=models.CASCADE) 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): def __str__(self):
return f"{self.sale} - {self.product}" 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): class Payment(models.Model):
date_time = models.DateTimeField() date_time = models.DateTimeField()
type_payment = models.CharField( type_payment = models.CharField(

View File

@@ -2,7 +2,13 @@ from rest_framework import serializers
from .models.sales import Sale, SaleLine from .models.sales import Sale, SaleLine
from .models.customers import Customer 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.products import Product, ProductCategory
from .models.payments import ReconciliationJar 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 ProductSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Product model = Product

View File

@@ -34,6 +34,16 @@ class TestAPI(APITestCase, LoginMixin):
self.assertEqual(sale.id, content["id"]) self.assertEqual(sale.id, content["id"])
self.assertIsNone(sale.external_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): def test_create_sale_with_decimal(self):
response = self._create_sale_with_decimal() response = self._create_sale_with_decimal()
content = json.loads(response.content.decode("utf-8")) content = json.loads(response.content.decode("utf-8"))

View File

@@ -8,6 +8,9 @@ app_name = "don_confiao"
router = DefaultRouter() router = DefaultRouter()
router.register(r"sales", api_views.SaleView, basename="sale") 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"customers", api_views.CustomerView, basename="customer")
router.register(r"products", api_views.ProductView, basename="product") router.register(r"products", api_views.ProductView, basename="product")
router.register( router.register(
@@ -23,6 +26,17 @@ urlpatterns = [
api_views.SaleSummary.as_view(), api_views.SaleSummary.as_view(),
name="purchase_json_summary", 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/", include(router.urls)),
path( path(
"api/importar_productos_de_tryton", "api/importar_productos_de_tryton",
@@ -39,17 +53,6 @@ urlpatterns = [
api_views.SalesToTrytonView.as_view(), api_views.SalesToTrytonView.as_view(),
name="send_tryton", 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( path(
"api/admin_code/validate/<code>", "api/admin_code/validate/<code>",
api_views.AdminCodeValidateView.as_view(), api_views.AdminCodeValidateView.as_view(),