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 .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)

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.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

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
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(

View File

@@ -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

View File

@@ -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"))

View File

@@ -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(),