Add invoice cancelled status, treasury event name, and voucher number backfill.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
31
accounting/migrations/0007_backfill_voucher_numbers.py
Normal file
31
accounting/migrations/0007_backfill_voucher_numbers.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_voucher_numbers(apps, schema_editor):
|
||||||
|
Voucher = apps.get_model('accounting', 'Voucher')
|
||||||
|
|
||||||
|
max_number = (
|
||||||
|
Voucher.objects.exclude(number__lte=0)
|
||||||
|
.order_by('-number')
|
||||||
|
.values_list('number', flat=True)
|
||||||
|
.first()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
missing_number_vouchers = Voucher.objects.filter(number__lte=0).order_by('created_at', 'id')
|
||||||
|
|
||||||
|
next_number = max_number + 1
|
||||||
|
for voucher in missing_number_vouchers:
|
||||||
|
voucher.number = next_number
|
||||||
|
voucher.save(update_fields=['number'])
|
||||||
|
next_number += 1
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('accounting', '0006_treasury_fee_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(backfill_voucher_numbers, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
25
accounting/migrations/0008_invoice_cancelled_status.py
Normal file
25
accounting/migrations/0008_invoice_cancelled_status.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounting', '0007_backfill_voucher_numbers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('confirmed', 'Confirmed'),
|
||||||
|
('paid', 'Paid'),
|
||||||
|
('cancelled', 'Cancelled'),
|
||||||
|
],
|
||||||
|
default='draft',
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
30
accounting/migrations/0009_treasury_event_name.py
Normal file
30
accounting/migrations/0009_treasury_event_name.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
"""
|
||||||
|
DB already has event_name (NOT NULL) from an earlier manual/out-of-band change.
|
||||||
|
Sync Django state and ensure NULLs become empty string so inserts never omit the column.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounting', '0008_invoice_cancelled_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.SeparateDatabaseAndState(
|
||||||
|
state_operations=[
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='treasurytxn',
|
||||||
|
name='event_name',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=200),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
database_operations=[
|
||||||
|
migrations.RunSQL(
|
||||||
|
sql="UPDATE accounting_treasury_txns SET event_name = '' WHERE event_name IS NULL;",
|
||||||
|
reverse_sql=migrations.RunSQL.noop,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -117,6 +117,7 @@ class Voucher(models.Model):
|
|||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
number = models.PositiveIntegerField(default=0)
|
number = models.PositiveIntegerField(default=0)
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
|
regarding = models.CharField(max_length=255, blank=True, default='')
|
||||||
description = models.TextField(blank=True, default='')
|
description = models.TextField(blank=True, default='')
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
|
||||||
source = models.CharField(max_length=120, blank=True, default='', db_index=True)
|
source = models.CharField(max_length=120, blank=True, default='', db_index=True)
|
||||||
@@ -208,6 +209,7 @@ class Invoice(models.Model):
|
|||||||
('draft', 'Draft'),
|
('draft', 'Draft'),
|
||||||
('confirmed', 'Confirmed'),
|
('confirmed', 'Confirmed'),
|
||||||
('paid', 'Paid'),
|
('paid', 'Paid'),
|
||||||
|
('cancelled', 'Cancelled'),
|
||||||
]
|
]
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
@@ -250,6 +252,7 @@ class InvoiceLine(models.Model):
|
|||||||
unit_price = models.FloatField(default=0)
|
unit_price = models.FloatField(default=0)
|
||||||
discount = models.FloatField(default=0)
|
discount = models.FloatField(default=0)
|
||||||
tax_rate = models.FloatField(default=0)
|
tax_rate = models.FloatField(default=0)
|
||||||
|
event_name = models.CharField(max_length=255, blank=True, default='')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'accounting_invoice_lines'
|
db_table = 'accounting_invoice_lines'
|
||||||
@@ -285,6 +288,7 @@ class TreasuryTxn(models.Model):
|
|||||||
owner_net_amount = models.FloatField(default=0)
|
owner_net_amount = models.FloatField(default=0)
|
||||||
reference = models.CharField(max_length=120, blank=True, default='')
|
reference = models.CharField(max_length=120, blank=True, default='')
|
||||||
description = models.TextField(blank=True, default='')
|
description = models.TextField(blank=True, default='')
|
||||||
|
event_name = models.CharField(max_length=200, blank=True, default='')
|
||||||
source = models.CharField(max_length=120, blank=True, default='')
|
source = models.CharField(max_length=120, blank=True, default='')
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|||||||
@@ -73,14 +73,29 @@ class VoucherLineSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class VoucherSerializer(serializers.ModelSerializer):
|
class VoucherSerializer(serializers.ModelSerializer):
|
||||||
lines = VoucherLineSerializer(many=True)
|
lines = VoucherLineSerializer(many=True)
|
||||||
|
regarding = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Voucher
|
model = Voucher
|
||||||
fields = ['id', 'number', 'date', 'description', 'status', 'source', 'lines']
|
fields = ['id', 'number', 'date', 'regarding', 'description', 'status', 'source', 'lines']
|
||||||
|
read_only_fields = ['number']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _next_number() -> int:
|
||||||
|
last_number = (
|
||||||
|
Voucher.objects.select_for_update()
|
||||||
|
.order_by('-number')
|
||||||
|
.values_list('number', flat=True)
|
||||||
|
.first()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
return last_number + 1
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
lines = validated_data.pop('lines', [])
|
lines = validated_data.pop('lines', [])
|
||||||
|
validated_data.setdefault('regarding', '')
|
||||||
|
validated_data['number'] = self._next_number()
|
||||||
voucher = Voucher.objects.create(**validated_data)
|
voucher = Voucher.objects.create(**validated_data)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line.pop('id', None)
|
line.pop('id', None)
|
||||||
@@ -90,6 +105,12 @@ class VoucherSerializer(serializers.ModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
lines = validated_data.pop('lines', None)
|
lines = validated_data.pop('lines', None)
|
||||||
|
# Keep voucher numbers immutable after creation.
|
||||||
|
validated_data.pop('number', None)
|
||||||
|
if 'regarding' in validated_data and validated_data['regarding'] is None:
|
||||||
|
validated_data['regarding'] = ''
|
||||||
|
if instance.number <= 0:
|
||||||
|
instance.number = self._next_number()
|
||||||
for attr, value in validated_data.items():
|
for attr, value in validated_data.items():
|
||||||
setattr(instance, attr, value)
|
setattr(instance, attr, value)
|
||||||
instance.save()
|
instance.save()
|
||||||
@@ -123,10 +144,11 @@ class InvoiceLineSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
unitPrice = serializers.FloatField(source='unit_price', required=False)
|
unitPrice = serializers.FloatField(source='unit_price', required=False)
|
||||||
taxRate = serializers.FloatField(source='tax_rate', required=False)
|
taxRate = serializers.FloatField(source='tax_rate', required=False)
|
||||||
|
eventName = serializers.CharField(source='event_name', required=False, allow_blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
model = InvoiceLine
|
||||||
fields = ['id', 'productId', 'quantity', 'unitPrice', 'discount', 'taxRate']
|
fields = ['id', 'productId', 'quantity', 'unitPrice', 'discount', 'taxRate', 'eventName']
|
||||||
|
|
||||||
|
|
||||||
class InvoiceSerializer(serializers.ModelSerializer):
|
class InvoiceSerializer(serializers.ModelSerializer):
|
||||||
@@ -155,6 +177,7 @@ class InvoiceSerializer(serializers.ModelSerializer):
|
|||||||
invoice = Invoice.objects.create(**validated_data)
|
invoice = Invoice.objects.create(**validated_data)
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line.pop('id', None)
|
line.pop('id', None)
|
||||||
|
line.setdefault('event_name', '')
|
||||||
InvoiceLine.objects.create(invoice=invoice, **line)
|
InvoiceLine.objects.create(invoice=invoice, **line)
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
@@ -168,6 +191,7 @@ class InvoiceSerializer(serializers.ModelSerializer):
|
|||||||
instance.lines.all().delete()
|
instance.lines.all().delete()
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line.pop('id', None)
|
line.pop('id', None)
|
||||||
|
line.setdefault('event_name', '')
|
||||||
InvoiceLine.objects.create(invoice=instance, **line)
|
InvoiceLine.objects.create(invoice=instance, **line)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
@@ -181,13 +205,14 @@ class TreasuryTxnSerializer(serializers.ModelSerializer):
|
|||||||
taxAmount = serializers.FloatField(source='tax_amount', required=False)
|
taxAmount = serializers.FloatField(source='tax_amount', required=False)
|
||||||
platformProfitAmount = serializers.FloatField(source='platform_profit_amount', required=False)
|
platformProfitAmount = serializers.FloatField(source='platform_profit_amount', required=False)
|
||||||
ownerNetAmount = serializers.FloatField(source='owner_net_amount', required=False)
|
ownerNetAmount = serializers.FloatField(source='owner_net_amount', required=False)
|
||||||
|
eventName = serializers.CharField(source='event_name', required=False, allow_blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TreasuryTxn
|
model = TreasuryTxn
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'number', 'kind', 'method', 'date', 'partyId', 'amount',
|
'id', 'number', 'kind', 'method', 'date', 'partyId', 'amount',
|
||||||
'taxAmount', 'platformProfitAmount', 'ownerNetAmount',
|
'taxAmount', 'platformProfitAmount', 'ownerNetAmount',
|
||||||
'reference', 'description', 'source',
|
'reference', 'description', 'eventName', 'source',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user