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 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)
|
||||||
|
|||||||
@@ -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.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
|
||||||
|
|||||||
@@ -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
|
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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user