from django.db import transaction from rest_framework import serializers from .models import ( Account, CompanySettings, Employee, Invoice, InvoiceLine, Party, Payslip, Product, TreasuryTxn, Voucher, VoucherLine, ) class AccountSerializer(serializers.ModelSerializer): parentId = serializers.PrimaryKeyRelatedField( source='parent', queryset=Account.objects.all(), allow_null=True, required=False ) isGroup = serializers.BooleanField(source='is_group', required=False) openingBalance = serializers.FloatField(source='opening_balance', required=False) class Meta: model = Account fields = ['id', 'code', 'name', 'type', 'parentId', 'isGroup', 'openingBalance'] class PartySerializer(serializers.ModelSerializer): economicCode = serializers.CharField(source='economic_code', required=False, allow_blank=True) openingBalance = serializers.FloatField(source='opening_balance', required=False) class Meta: model = Party fields = ['id', 'kind', 'name', 'phone', 'economicCode', 'address', 'openingBalance'] class ProductSerializer(serializers.ModelSerializer): salePrice = serializers.FloatField(source='sale_price', required=False) purchasePrice = serializers.FloatField(source='purchase_price', required=False) reorderLevel = serializers.FloatField(source='reorder_level', required=False) class Meta: model = Product fields = ['id', 'code', 'name', 'unit', 'salePrice', 'purchasePrice', 'stock', 'reorderLevel'] class VoucherLineSerializer(serializers.ModelSerializer): # Lenient: accepts client-generated ids (ignored on write, recreated server-side). id = serializers.CharField(required=False) accountId = serializers.PrimaryKeyRelatedField(source='account', queryset=Account.objects.all()) class Meta: model = VoucherLine fields = ['id', 'accountId', 'description', 'debit', 'credit'] class VoucherSerializer(serializers.ModelSerializer): lines = VoucherLineSerializer(many=True) class Meta: model = Voucher fields = ['id', 'number', 'date', 'description', 'status', 'source', 'lines'] @transaction.atomic def create(self, validated_data): lines = validated_data.pop('lines', []) voucher = Voucher.objects.create(**validated_data) for line in lines: line.pop('id', None) VoucherLine.objects.create(voucher=voucher, **line) return voucher @transaction.atomic def update(self, instance, validated_data): lines = validated_data.pop('lines', None) for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() if lines is not None: instance.lines.all().delete() for line in lines: line.pop('id', None) VoucherLine.objects.create(voucher=instance, **line) return instance class InvoiceLineSerializer(serializers.ModelSerializer): # Lenient: accepts client-generated ids (ignored on write, recreated server-side). id = serializers.CharField(required=False) productId = serializers.PrimaryKeyRelatedField( source='product', queryset=Product.objects.all(), allow_null=True, required=False ) unitPrice = serializers.FloatField(source='unit_price', required=False) taxRate = serializers.FloatField(source='tax_rate', required=False) class Meta: model = InvoiceLine fields = ['id', 'productId', 'quantity', 'unitPrice', 'discount', 'taxRate'] class InvoiceSerializer(serializers.ModelSerializer): partyId = serializers.PrimaryKeyRelatedField( source='party', queryset=Party.objects.all(), allow_null=True, required=False ) lines = InvoiceLineSerializer(many=True) class Meta: model = Invoice fields = ['id', 'kind', 'number', 'partyId', 'date', 'status', 'note', 'lines'] @transaction.atomic def create(self, validated_data): lines = validated_data.pop('lines', []) invoice = Invoice.objects.create(**validated_data) for line in lines: line.pop('id', None) InvoiceLine.objects.create(invoice=invoice, **line) return invoice @transaction.atomic def update(self, instance, validated_data): lines = validated_data.pop('lines', None) for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() if lines is not None: instance.lines.all().delete() for line in lines: line.pop('id', None) InvoiceLine.objects.create(invoice=instance, **line) return instance class TreasuryTxnSerializer(serializers.ModelSerializer): partyId = serializers.PrimaryKeyRelatedField( source='party', queryset=Party.objects.all(), allow_null=True, required=False ) class Meta: model = TreasuryTxn fields = ['id', 'number', 'kind', 'method', 'date', 'partyId', 'amount', 'reference', 'description'] class EmployeeSerializer(serializers.ModelSerializer): baseSalary = serializers.FloatField(source='base_salary', required=False) insuranceNo = serializers.CharField(source='insurance_no', required=False, allow_blank=True) hireDate = serializers.DateField(source='hire_date', required=False, allow_null=True) class Meta: model = Employee fields = ['id', 'code', 'name', 'position', 'baseSalary', 'insuranceNo', 'hireDate', 'active'] class PayslipSerializer(serializers.ModelSerializer): employeeId = serializers.PrimaryKeyRelatedField(source='employee', queryset=Employee.objects.all()) baseSalary = serializers.FloatField(source='base_salary', required=False) class Meta: model = Payslip fields = [ 'id', 'employeeId', 'period', 'baseSalary', 'overtime', 'bonus', 'deductions', 'insurance', 'tax', ] class CompanySettingsSerializer(serializers.ModelSerializer): economicCode = serializers.CharField(source='economic_code', required=False, allow_blank=True) fiscalYear = serializers.CharField(source='fiscal_year', required=False, allow_blank=True) baseCurrency = serializers.CharField(source='base_currency', required=False, allow_blank=True) taxRate = serializers.FloatField(source='tax_rate', required=False) voucherAccounts = serializers.SerializerMethodField() _MAPPING_FIELDS = { 'bankAccountId': 'bank_account_id', 'ownerPayableAccountId': 'owner_payable_account_id', 'customerPayableAccountId': 'customer_payable_account_id', 'ownerShareAccountId': 'owner_share_account_id', 'salesIncomeAccountId': 'sales_income_account_id', } class Meta: model = CompanySettings fields = [ 'name', 'economicCode', 'fiscalYear', 'baseCurrency', 'taxRate', 'address', 'phone', 'voucherAccounts', ] def get_voucherAccounts(self, obj): def to_id(value): return str(value) if value else '' return { 'bankAccountId': to_id(obj.bank_account_id), 'ownerPayableAccountId': to_id(obj.owner_payable_account_id), 'customerPayableAccountId': to_id(obj.customer_payable_account_id), 'ownerShareAccountId': to_id(obj.owner_share_account_id), 'salesIncomeAccountId': to_id(obj.sales_income_account_id), } def _apply_voucher_accounts(self, instance): mapping = self.initial_data.get('voucherAccounts') if not isinstance(mapping, dict): return valid_ids = {str(v) for v in Account.objects.values_list('id', flat=True)} for camel_key, model_attr in self._MAPPING_FIELDS.items(): if camel_key not in mapping: continue raw = mapping.get(camel_key) setattr(instance, model_attr, raw if raw in valid_ids else None) @transaction.atomic def update(self, instance, validated_data): for attr, value in validated_data.items(): setattr(instance, attr, value) self._apply_voucher_accounts(instance) instance.save() return instance