Add sales types, party adjustments, FunZone party fields, and treasury fee support
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
78
accounting/migrations/0003_sales_module.py
Normal file
78
accounting/migrations/0003_sales_module.py
Normal 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',
|
||||
),
|
||||
),
|
||||
]
|
||||
84
accounting/migrations/0004_party_funzone_fields.py
Normal file
84
accounting/migrations/0004_party_funzone_fields.py
Normal 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',
|
||||
),
|
||||
),
|
||||
]
|
||||
16
accounting/migrations/0005_treasury_source.py
Normal file
16
accounting/migrations/0005_treasury_source.py
Normal 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),
|
||||
),
|
||||
]
|
||||
26
accounting/migrations/0006_treasury_fee_fields.py
Normal file
26
accounting/migrations/0006_treasury_fee_fields.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user