Add sales types, party adjustments, FunZone party fields, and treasury fee support

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Shayan Azadi
2026-07-03 03:15:52 +03:30
parent 5dd29c41d6
commit 5fb79fb7f0
8 changed files with 366 additions and 3 deletions

View File

@@ -0,0 +1,78 @@
# Generated manually for sales module extensions
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('accounting', '0002_seed_chart_of_accounts'),
]
operations = [
migrations.CreateModel(
name='SalesType',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('code', models.CharField(db_index=True, max_length=50)),
('name', models.CharField(max_length=150)),
('description', models.TextField(blank=True, default='')),
('active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'accounting_sales_types',
'ordering': ['code'],
},
),
migrations.CreateModel(
name='PartyAdjustment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('number', models.PositiveIntegerField(default=0)),
('kind', models.CharField(choices=[('debit', 'Debit'), ('credit', 'Credit')], max_length=20)),
('date', models.DateField()),
('amount', models.FloatField(default=0)),
('description', models.TextField(blank=True, default='')),
('created_at', models.DateTimeField(auto_now_add=True)),
('party', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adjustments', to='accounting.party')),
],
options={
'db_table': 'accounting_party_adjustments',
'ordering': ['-date', '-number'],
},
),
migrations.AddField(
model_name='invoice',
name='document_type',
field=models.CharField(
choices=[('proforma', 'Proforma'), ('invoice', 'Invoice'), ('return', 'Return')],
default='invoice',
max_length=20,
),
),
migrations.AddField(
model_name='invoice',
name='related_invoice',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='derived_docs',
to='accounting.invoice',
),
),
migrations.AddField(
model_name='invoice',
name='sales_type',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='invoices',
to='accounting.salestype',
),
),
]

View File

@@ -0,0 +1,84 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounting', '0003_sales_module'),
]
operations = [
migrations.AddField(
model_name='party',
name='external_id',
field=models.UUIDField(blank=True, db_index=True, null=True),
),
migrations.AddField(
model_name='party',
name='external_source',
field=models.CharField(blank=True, default='', max_length=30),
),
migrations.AddField(
model_name='party',
name='first_name',
field=models.CharField(blank=True, default='', max_length=100),
),
migrations.AddField(
model_name='party',
name='last_name',
field=models.CharField(blank=True, default='', max_length=100),
),
migrations.AddField(
model_name='party',
name='username',
field=models.CharField(blank=True, default='', max_length=100),
),
migrations.AddField(
model_name='party',
name='email',
field=models.EmailField(blank=True, default='', max_length=254),
),
migrations.AddField(
model_name='party',
name='national_code',
field=models.CharField(blank=True, default='', max_length=20),
),
migrations.AddField(
model_name='party',
name='iban',
field=models.CharField(blank=True, default='', max_length=34),
),
migrations.AddField(
model_name='party',
name='wallet_balance',
field=models.FloatField(default=0),
),
migrations.AddField(
model_name='party',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='party',
name='is_verified',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='party',
name='birthday',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='party',
name='venues',
field=models.JSONField(blank=True, default=list),
),
migrations.AddConstraint(
model_name='party',
constraint=models.UniqueConstraint(
condition=models.Q(external_id__isnull=False),
fields=('external_id', 'external_source'),
name='unique_external_party',
),
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounting', '0004_party_funzone_fields'),
]
operations = [
migrations.AddField(
model_name='treasurytxn',
name='source',
field=models.CharField(blank=True, default='', max_length=120),
),
]

View File

@@ -0,0 +1,26 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounting', '0005_treasury_source'),
]
operations = [
migrations.AddField(
model_name='treasurytxn',
name='tax_amount',
field=models.FloatField(default=0),
),
migrations.AddField(
model_name='treasurytxn',
name='platform_profit_amount',
field=models.FloatField(default=0),
),
migrations.AddField(
model_name='treasurytxn',
name='owner_net_amount',
field=models.FloatField(default=0),
),
]

View File

@@ -40,6 +40,11 @@ class Party(models.Model):
('customer', 'Customer'),
('supplier', 'Supplier'),
]
EXTERNAL_SOURCES = [
('', 'Manual'),
('funzone_customer', 'FunZone Customer'),
('funzone_owner', 'FunZone Owner'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
kind = models.CharField(max_length=20, choices=PARTY_KINDS)
@@ -48,11 +53,33 @@ class Party(models.Model):
economic_code = models.CharField(max_length=50, blank=True, default='')
address = models.TextField(blank=True, default='')
opening_balance = models.FloatField(default=0)
external_id = models.UUIDField(null=True, blank=True, db_index=True)
external_source = models.CharField(
max_length=30, choices=EXTERNAL_SOURCES, blank=True, default=''
)
first_name = models.CharField(max_length=100, blank=True, default='')
last_name = models.CharField(max_length=100, blank=True, default='')
username = models.CharField(max_length=100, blank=True, default='')
email = models.EmailField(blank=True, default='')
national_code = models.CharField(max_length=20, blank=True, default='')
iban = models.CharField(max_length=34, blank=True, default='')
wallet_balance = models.FloatField(default=0)
is_active = models.BooleanField(default=True)
is_verified = models.BooleanField(default=False)
birthday = models.DateField(null=True, blank=True)
venues = models.JSONField(default=list, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'accounting_parties'
ordering = ['name']
constraints = [
models.UniqueConstraint(
fields=['external_id', 'external_source'],
condition=models.Q(external_id__isnull=False),
name='unique_external_party',
),
]
def __str__(self):
return self.name
@@ -120,6 +147,51 @@ class VoucherLine(models.Model):
return f"Line {self.account_id} D:{self.debit} C:{self.credit}"
class SalesType(models.Model):
"""A sales category (نوع فروش) referenced by sales documents."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
code = models.CharField(max_length=50, db_index=True)
name = models.CharField(max_length=150)
description = models.TextField(blank=True, default='')
active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'accounting_sales_types'
ordering = ['code']
def __str__(self):
return f"{self.code} - {self.name}"
class PartyAdjustment(models.Model):
"""Debit/credit notice adjusting a customer receivable (اعلامیه بدهکار/بستانکار)."""
ADJUSTMENT_KINDS = [
('debit', 'Debit'),
('credit', 'Credit'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
number = models.PositiveIntegerField(default=0)
party = models.ForeignKey(
Party, on_delete=models.CASCADE, related_name='adjustments'
)
kind = models.CharField(max_length=20, choices=ADJUSTMENT_KINDS)
date = models.DateField()
amount = models.FloatField(default=0)
description = models.TextField(blank=True, default='')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'accounting_party_adjustments'
ordering = ['-date', '-number']
def __str__(self):
return f"{self.kind} notice #{self.number}"
class Invoice(models.Model):
"""A sales or purchase invoice (فاکتور)."""
@@ -127,6 +199,11 @@ class Invoice(models.Model):
('sale', 'Sale'),
('purchase', 'Purchase'),
]
DOCUMENT_TYPES = [
('proforma', 'Proforma'),
('invoice', 'Invoice'),
('return', 'Return'),
]
STATUS_CHOICES = [
('draft', 'Draft'),
('confirmed', 'Confirmed'),
@@ -135,10 +212,19 @@ class Invoice(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
kind = models.CharField(max_length=20, choices=INVOICE_KINDS)
document_type = models.CharField(
max_length=20, choices=DOCUMENT_TYPES, default='invoice'
)
number = models.PositiveIntegerField(default=0)
party = models.ForeignKey(
Party, on_delete=models.SET_NULL, null=True, blank=True, related_name='invoices'
)
sales_type = models.ForeignKey(
SalesType, on_delete=models.SET_NULL, null=True, blank=True, related_name='invoices'
)
related_invoice = models.ForeignKey(
'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='derived_docs'
)
date = models.DateField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
note = models.TextField(blank=True, default='')
@@ -194,8 +280,12 @@ class TreasuryTxn(models.Model):
Party, on_delete=models.SET_NULL, null=True, blank=True, related_name='treasury_txns'
)
amount = models.FloatField(default=0)
tax_amount = models.FloatField(default=0)
platform_profit_amount = models.FloatField(default=0)
owner_net_amount = models.FloatField(default=0)
reference = models.CharField(max_length=120, blank=True, default='')
description = models.TextField(blank=True, default='')
source = models.CharField(max_length=120, blank=True, default='')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:

View File

@@ -8,8 +8,10 @@ from .models import (
Invoice,
InvoiceLine,
Party,
PartyAdjustment,
Payslip,
Product,
SalesType,
TreasuryTxn,
Voucher,
VoucherLine,
@@ -31,10 +33,22 @@ class AccountSerializer(serializers.ModelSerializer):
class PartySerializer(serializers.ModelSerializer):
economicCode = serializers.CharField(source='economic_code', required=False, allow_blank=True)
openingBalance = serializers.FloatField(source='opening_balance', required=False)
externalId = serializers.UUIDField(source='external_id', required=False, allow_null=True)
externalSource = serializers.CharField(source='external_source', required=False, allow_blank=True)
firstName = serializers.CharField(source='first_name', required=False, allow_blank=True)
lastName = serializers.CharField(source='last_name', required=False, allow_blank=True)
nationalCode = serializers.CharField(source='national_code', required=False, allow_blank=True)
walletBalance = serializers.FloatField(source='wallet_balance', required=False)
isActive = serializers.BooleanField(source='is_active', required=False)
isVerified = serializers.BooleanField(source='is_verified', required=False)
class Meta:
model = Party
fields = ['id', 'kind', 'name', 'phone', 'economicCode', 'address', 'openingBalance']
fields = [
'id', 'kind', 'name', 'phone', 'economicCode', 'address', 'openingBalance',
'externalId', 'externalSource', 'firstName', 'lastName', 'username', 'email',
'nationalCode', 'iban', 'walletBalance', 'isActive', 'isVerified', 'birthday', 'venues',
]
class ProductSerializer(serializers.ModelSerializer):
@@ -87,6 +101,20 @@ class VoucherSerializer(serializers.ModelSerializer):
return instance
class SalesTypeSerializer(serializers.ModelSerializer):
class Meta:
model = SalesType
fields = ['id', 'code', 'name', 'description', 'active']
class PartyAdjustmentSerializer(serializers.ModelSerializer):
partyId = serializers.PrimaryKeyRelatedField(source='party', queryset=Party.objects.all())
class Meta:
model = PartyAdjustment
fields = ['id', 'number', 'partyId', 'kind', 'date', 'amount', 'description']
class InvoiceLineSerializer(serializers.ModelSerializer):
# Lenient: accepts client-generated ids (ignored on write, recreated server-side).
id = serializers.CharField(required=False)
@@ -105,11 +133,21 @@ class InvoiceSerializer(serializers.ModelSerializer):
partyId = serializers.PrimaryKeyRelatedField(
source='party', queryset=Party.objects.all(), allow_null=True, required=False
)
documentType = serializers.CharField(source='document_type', required=False)
salesTypeId = serializers.PrimaryKeyRelatedField(
source='sales_type', queryset=SalesType.objects.all(), allow_null=True, required=False
)
relatedInvoiceId = serializers.PrimaryKeyRelatedField(
source='related_invoice', queryset=Invoice.objects.all(), allow_null=True, required=False
)
lines = InvoiceLineSerializer(many=True)
class Meta:
model = Invoice
fields = ['id', 'kind', 'number', 'partyId', 'date', 'status', 'note', 'lines']
fields = [
'id', 'kind', 'documentType', 'number', 'partyId', 'salesTypeId',
'relatedInvoiceId', 'date', 'status', 'note', 'lines',
]
@transaction.atomic
def create(self, validated_data):
@@ -139,9 +177,18 @@ class TreasuryTxnSerializer(serializers.ModelSerializer):
source='party', queryset=Party.objects.all(), allow_null=True, required=False
)
amount = serializers.FloatField(required=False)
taxAmount = serializers.FloatField(source='tax_amount', required=False)
platformProfitAmount = serializers.FloatField(source='platform_profit_amount', required=False)
ownerNetAmount = serializers.FloatField(source='owner_net_amount', required=False)
class Meta:
model = TreasuryTxn
fields = ['id', 'number', 'kind', 'method', 'date', 'partyId', 'amount', 'reference', 'description']
fields = [
'id', 'number', 'kind', 'method', 'date', 'partyId', 'amount',
'taxAmount', 'platformProfitAmount', 'ownerNetAmount',
'reference', 'description', 'source',
]
class EmployeeSerializer(serializers.ModelSerializer):

View File

@@ -6,9 +6,11 @@ from .views import (
CompanySettingsView,
EmployeeViewSet,
InvoiceViewSet,
PartyAdjustmentViewSet,
PartyViewSet,
PayslipViewSet,
ProductViewSet,
SalesTypeViewSet,
TreasuryTxnViewSet,
VoucherViewSet,
reset_accounting_data,
@@ -20,6 +22,8 @@ router.register(r'parties', PartyViewSet, basename='accounting-party')
router.register(r'products', ProductViewSet, basename='accounting-product')
router.register(r'vouchers', VoucherViewSet, basename='accounting-voucher')
router.register(r'invoices', InvoiceViewSet, basename='accounting-invoice')
router.register(r'sales-types', SalesTypeViewSet, basename='accounting-sales-type')
router.register(r'party-adjustments', PartyAdjustmentViewSet, basename='accounting-party-adjustment')
router.register(r'treasury', TreasuryTxnViewSet, basename='accounting-treasury')
router.register(r'employees', EmployeeViewSet, basename='accounting-employee')
router.register(r'payslips', PayslipViewSet, basename='accounting-payslip')

View File

@@ -10,8 +10,10 @@ from .models import (
Employee,
Invoice,
Party,
PartyAdjustment,
Payslip,
Product,
SalesType,
TreasuryTxn,
Voucher,
)
@@ -22,9 +24,11 @@ from .serializers import (
CompanySettingsSerializer,
EmployeeSerializer,
InvoiceSerializer,
PartyAdjustmentSerializer,
PartySerializer,
PayslipSerializer,
ProductSerializer,
SalesTypeSerializer,
TreasuryTxnSerializer,
VoucherSerializer,
)
@@ -66,6 +70,18 @@ class InvoiceViewSet(_AdminModelViewSet):
pagination_class = None
class SalesTypeViewSet(_AdminModelViewSet):
queryset = SalesType.objects.all()
serializer_class = SalesTypeSerializer
pagination_class = None
class PartyAdjustmentViewSet(_AdminModelViewSet):
queryset = PartyAdjustment.objects.all()
serializer_class = PartyAdjustmentSerializer
pagination_class = None
class TreasuryTxnViewSet(_AdminModelViewSet):
queryset = TreasuryTxn.objects.all()
serializer_class = TreasuryTxnSerializer
@@ -117,6 +133,8 @@ def reset_accounting_data(request):
"""Wipes all accounting data and recreates a clean, zeroed starting point."""
Voucher.objects.all().delete()
Invoice.objects.all().delete()
PartyAdjustment.objects.all().delete()
SalesType.objects.all().delete()
TreasuryTxn.objects.all().delete()
Payslip.objects.all().delete()
Employee.objects.all().delete()