class PlotSearchSerializerBase( EnumSupportSerializerMixin, FieldPermissionsSerializerMixin, serializers.ModelSerializer, ): id = serializers.ReadOnlyField() type = PlotSearchTypeSerializer(source="subtype.plot_search_type", read_only=True) subtype = PlotSearchSubtypeSerializer() stage = PlotSearchStageSerializer() form = InstanceDictPrimaryKeyRelatedField( instance_class=Form, queryset=Form.objects.all(), related_serializer=FormSerializer, required=False, allow_null=True, ) decisions = InstanceDictPrimaryKeyRelatedField( instance_class=Decision, queryset=Decision.objects.all(), related_serializer=DecisionSerializer, required=False, allow_null=True, many=True, ) class Meta: model = PlotSearch fields = "__all__"
class InvoiceRowCreateUpdateSerializer(FieldPermissionsSerializerMixin, serializers.ModelSerializer): id = serializers.IntegerField(required=False) tenant = InstanceDictPrimaryKeyRelatedField(instance_class=Tenant, queryset=Tenant.objects.all(), related_serializer=TenantSerializer, required=False, allow_null=True) receivable_type = InstanceDictPrimaryKeyRelatedField(instance_class=ReceivableType, queryset=ReceivableType.objects.all(), related_serializer=ReceivableTypeSerializer) class Meta: model = InvoiceRow fields = ('id', 'tenant', 'receivable_type', 'billing_period_start_date', 'billing_period_end_date', 'description', 'amount')
class InvoiceUpdateSerializer(UpdateNestedMixin, EnumSupportSerializerMixin, FieldPermissionsSerializerMixin, serializers.ModelSerializer): id = serializers.ReadOnlyField() recipient = InstanceDictPrimaryKeyRelatedField( instance_class=Contact, queryset=Contact.objects.all(), related_serializer=ContactSerializer) rows = InvoiceRowCreateUpdateSerializer(many=True) payments = InvoicePaymentCreateUpdateSerializer(many=True, required=False, allow_null=True) def validate(self, attrs): if self.instance.sent_to_sap_at: raise ValidationError( _("Can't edit invoices that have been sent to SAP")) return super().validate(attrs) def update(self, instance, validated_data): instance = super().update(instance, validated_data) instance.update_amounts() if instance.credited_invoice: instance.credited_invoice.update_amounts() return instance class Meta: model = Invoice exclude = ('deleted', ) read_only_fields = ('generated', 'sent_to_sap_at', 'sap_id', 'state', 'adjusted_due_date', 'credit_invoices')
class CreateChargeInvoiceRowSerializer(serializers.Serializer): amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False) receivable_type = InstanceDictPrimaryKeyRelatedField( instance_class=ReceivableType, queryset=ReceivableType.objects.all(), related_serializer=ReceivableTypeSerializer, )
class SendEmailSerializer(EnumSupportSerializerMixin, serializers.Serializer): type = EnumField(enum=EmailLogType, required=True) recipients = PrimaryKeyRelatedField(many=True, queryset=User.objects.all()) text = serializers.CharField() lease = InstanceDictPrimaryKeyRelatedField( instance_class=Lease, queryset=Lease.objects.all(), related_serializer=LeaseSuccinctSerializer, required=False)
def __init__(self, instance=None, data=empty, **kwargs): super().__init__(instance=instance, data=data, **kwargs) # Lease field must be added dynamically to prevent circular imports from leasing.serializers.lease import LeaseSuccinctSerializer from leasing.models.lease import Lease self.fields['lease'] = InstanceDictPrimaryKeyRelatedField(instance_class=Lease, queryset=Lease.objects.all(), related_serializer=LeaseSuccinctSerializer)
class InvoiceCreateSerializer(UpdateNestedMixin, EnumSupportSerializerMixin, FieldPermissionsSerializerMixin, serializers.ModelSerializer): id = serializers.ReadOnlyField() recipient = InstanceDictPrimaryKeyRelatedField( instance_class=Contact, queryset=Contact.objects.all(), related_serializer=ContactSerializer) rows = InvoiceRowCreateUpdateSerializer(many=True) payments = InvoicePaymentCreateUpdateSerializer(many=True, required=False, allow_null=True) # Make total_amount, billed_amount, and type not requided in serializer and set them in create() if needed total_amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False) billed_amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False) type = EnumField(enum=InvoiceType, required=False) def create(self, validated_data): validated_data['state'] = InvoiceState.OPEN if not validated_data.get('total_amount'): total_amount = Decimal(0) for row in validated_data.get('rows', []): total_amount += row.get('amount', Decimal(0)) validated_data['total_amount'] = total_amount if not validated_data.get('billed_amount'): billed_amount = Decimal(0) for row in validated_data.get('rows', []): billed_amount += row.get('amount', Decimal(0)) validated_data['billed_amount'] = billed_amount if not validated_data.get('type'): validated_data['type'] = InvoiceType.CHARGE return super().create(validated_data) class Meta: model = Invoice exclude = ('deleted', ) read_only_fields = ('number', 'generated', 'sent_to_sap_at', 'sap_id', 'state', 'adjusted_due_date', 'credit_invoices')
class InvoiceUpdateSerializer( UpdateNestedMixin, EnumSupportSerializerMixin, FieldPermissionsSerializerMixin, serializers.ModelSerializer, ): id = serializers.ReadOnlyField() recipient = InstanceDictPrimaryKeyRelatedField( instance_class=Contact, queryset=Contact.objects.all(), related_serializer=ContactSerializer, ) rows = InvoiceRowCreateUpdateSerializer(many=True) payments = InvoicePaymentCreateUpdateSerializer( many=True, required=False, allow_null=True ) def update(self, instance, validated_data): instance = super().update(instance, validated_data) instance.update_amounts() if instance.credited_invoice: instance.credited_invoice.update_amounts() return instance class Meta: model = Invoice exclude = ("deleted",) read_only_fields = ( "generated", "sent_to_sap_at", "sap_id", "state", "adjusted_due_date", "credit_invoices", "interest_invoices", )
class CreateChargeSerializer(serializers.Serializer): lease = InstanceDictPrimaryKeyRelatedField( instance_class=Lease, queryset=Lease.objects.all(), related_serializer=LeaseSuccinctSerializer) due_date = serializers.DateField() billing_period_start_date = serializers.DateField(required=False) billing_period_end_date = serializers.DateField(required=False) rows = serializers.ListSerializer(child=CreateChargeInvoiceRowSerializer(), required=True) notes = serializers.CharField(required=False) def to_representation(self, instance): if isinstance(instance, InvoiceSet): return InvoiceSetSerializer().to_representation(instance=instance) elif isinstance(instance, Invoice): return InvoiceSerializer().to_representation(instance=instance) def validate(self, data): if (data.get('billing_period_start_date') and not data.get('billing_period_end_date')) or ( not data.get('billing_period_start_date') and data.get('billing_period_end_date')): raise serializers.ValidationError( _("Both Billing period start and end are " "required if one of them is provided")) if data.get('billing_period_start_date', 0) > data.get( 'billing_period_end_date', 0): raise serializers.ValidationError( _("Billing period end must be the same or after the start")) return data def create(self, validated_data): today = timezone.now().date() lease = validated_data.get('lease') billing_period_start_date = validated_data.get( 'billing_period_start_date', today) billing_period_end_date = validated_data.get('billing_period_end_date', today) billing_period = (billing_period_start_date, billing_period_end_date) total_amount = sum( [row.get('amount') for row in validated_data.get('rows', [])]) # TODO: Handle possible exception shares = lease.get_tenant_shares_for_period(billing_period_start_date, billing_period_end_date) invoice = None invoiceset = None if len(shares.items()) > 1: invoiceset = InvoiceSet.objects.create( lease=lease, billing_period_start_date=billing_period_start_date, billing_period_end_date=billing_period_end_date) # TODO: check for periods without 1/1 shares for contact, share in shares.items(): invoice_row_data = [] billable_amount = Decimal(0) for tenant, overlaps in share.items(): for row in validated_data.get('rows', []): overlap_amount = Decimal(0) for overlap in overlaps: overlap_amount += fix_amount_for_overlap( row.get('amount', Decimal(0)), overlap, subtract_ranges_from_ranges([billing_period], [overlap])) share_amount = Decimal( overlap_amount * Decimal(tenant.share_numerator / tenant.share_denominator)).quantize( Decimal('.01'), rounding=ROUND_HALF_UP) billable_amount += share_amount invoice_row_data.append({ 'tenant': tenant, 'receivable_type': row.get('receivable_type'), 'billing_period_start_date': overlap[0], 'billing_period_end_date': overlap[1], 'amount': share_amount, }) invoice = Invoice.objects.create( type=InvoiceType.CHARGE, lease=lease, recipient=contact, due_date=validated_data.get('due_date'), invoicing_date=today, state=InvoiceState.OPEN, billing_period_start_date=billing_period_start_date, billing_period_end_date=billing_period_end_date, total_amount=total_amount, billed_amount=billable_amount, outstanding_amount=billable_amount, invoiceset=invoiceset, notes=validated_data.get('notes', ''), ) for invoice_row_datum in invoice_row_data: invoice_row_datum['invoice'] = invoice InvoiceRow.objects.create(**invoice_row_datum) if invoiceset: return invoiceset else: return invoice
class InvoiceCreateSerializer(UpdateNestedMixin, EnumSupportSerializerMixin, FieldPermissionsSerializerMixin, serializers.ModelSerializer): id = serializers.ReadOnlyField() recipient = InstanceDictPrimaryKeyRelatedField(instance_class=Contact, queryset=Contact.objects.all(), related_serializer=ContactSerializer, required=False) tenant = InstanceDictPrimaryKeyRelatedField(instance_class=Tenant, queryset=Tenant.objects.all(), related_serializer=TenantSerializer, required=False) rows = InvoiceRowCreateUpdateSerializer(many=True) payments = InvoicePaymentCreateUpdateSerializer(many=True, required=False, allow_null=True) # Make total_amount, billed_amount, and type not required in the serializer and set them in create() if needed total_amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False) billed_amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False) type = EnumField(enum=InvoiceType, required=False) def override_permission_check_field_name(self, field_name): if field_name == 'tenant': return 'recipient' return field_name def validate(self, attrs): if not bool(attrs.get('recipient')) ^ bool(attrs.get('tenant')): raise ValidationError(_('Either recipient or tenant is required.')) if attrs.get('tenant') and attrs.get('tenant') not in attrs.get('lease').tenants.all(): raise ValidationError(_('Tenant not found in lease')) return attrs def create(self, validated_data): validated_data['state'] = InvoiceState.OPEN if not validated_data.get('total_amount'): total_amount = Decimal(0) for row in validated_data.get('rows', []): total_amount += row.get('amount', Decimal(0)) validated_data['total_amount'] = total_amount if not validated_data.get('billed_amount'): billed_amount = Decimal(0) for row in validated_data.get('rows', []): billed_amount += row.get('amount', Decimal(0)) validated_data['billed_amount'] = billed_amount if not validated_data.get('type'): validated_data['type'] = InvoiceType.CHARGE if validated_data.get('tenant'): today = datetime.date.today() tenant = validated_data.pop('tenant') billing_tenantcontact = tenant.get_billing_tenantcontacts(today, today).first() if not billing_tenantcontact: raise ValidationError(_('Billing contact not found for tenant')) validated_data['recipient'] = billing_tenantcontact.contact for row in validated_data.get('rows', []): row['tenant'] = tenant invoice = super().create(validated_data) invoice.invoicing_date = timezone.now().date() invoice.outstanding_amount = validated_data['total_amount'] invoice.save() return invoice class Meta: model = Invoice exclude = ('deleted',) read_only_fields = ('number', 'generated', 'sent_to_sap_at', 'sap_id', 'state', 'adjusted_due_date', 'credit_invoices', 'interest_invoices')
class PlotSearchCreateSerializer(PlotSearchUpdateSerializer): subtype = InstanceDictPrimaryKeyRelatedField( instance_class=PlotSearchSubtype, queryset=PlotSearchSubtype.objects.all(), related_serializer=PlotSearchSubtypeSerializer, required=False, ) stage = InstanceDictPrimaryKeyRelatedField( instance_class=PlotSearchStage, queryset=PlotSearchStage.objects.all(), related_serializer=PlotSearchStageSerializer, required=False, ) preparer = InstanceDictPrimaryKeyRelatedField( instance_class=User, queryset=User.objects.all(), related_serializer=UserSerializer, required=False, ) plot_search_targets = PlotSearchTargetSerializer(many=True, required=False) plan_unit = PlanUnitSerializer(read_only=True) class Meta: model = PlotSearch fields = "__all__" @staticmethod def handle_targets(targets, plot_search): for target in targets: plan_unit = PlanUnit.objects.get(id=target.get("plan_unit_id")) plot_search_target = PlotSearchTarget.objects.create( plot_search=plot_search, plan_unit=plan_unit, target_type=target.get("target_type"), ) plot_search_target.save() if "info_links" in target: for link in target["info_links"]: link["plot_search_target"] = plot_search_target plot_search_target.info_links.add( PlotSearchTargetInfoLinkSerializer().create(link)) def create(self, validated_data): targets = None if "plot_search_targets" in validated_data: targets = validated_data.pop("plot_search_targets") decisions = None if "decisions" in validated_data: decisions = validated_data.pop("decisions") plot_search = PlotSearch.objects.create(**validated_data) if targets: self.handle_targets(targets, plot_search) if decisions: for decision in decisions: plot_search.decisions.add(decision) plot_search.save() return plot_search
class PlotSearchUpdateSerializer( UpdateNestedMixin, EnumSupportSerializerMixin, FieldPermissionsSerializerMixin, serializers.ModelSerializer, ): id = serializers.ReadOnlyField() name = serializers.CharField(required=True) type = InstanceDictPrimaryKeyRelatedField( instance_class=PlotSearchType, queryset=PlotSearchType.objects.all(), required=False, allow_null=True, ) subtype = InstanceDictPrimaryKeyRelatedField( instance_class=PlotSearchSubtype, queryset=PlotSearchSubtype.objects.all(), related_serializer=PlotSearchSubtypeSerializer, required=True, ) stage = InstanceDictPrimaryKeyRelatedField( instance_class=PlotSearchStage, queryset=PlotSearchStage.objects.all(), related_serializer=PlotSearchStageSerializer, required=True, ) preparer = InstanceDictPrimaryKeyRelatedField( instance_class=User, queryset=User.objects.all(), related_serializer=UserSerializer, required=True, ) plot_search_targets = PlotSearchTargetCreateUpdateSerializer( many=True, required=False) class Meta: model = PlotSearch fields = "__all__" def to_representation(self, instance): return PlotSearchRetrieveSerializer().to_representation(instance) @staticmethod def dict_to_instance(dictionary, model): if isinstance(dictionary, model): return dictionary instance, created = model.objects.get_or_create(id=dictionary["id"]) if created: for k, v in dictionary: setattr(instance, k, v) instance.save() return instance @staticmethod def handle_targets(targets, instance): exist_target_ids = [] for target in targets: target_id = target.get("id") target["plot_search"] = instance if target_id: plot_search_target = PlotSearchTarget.objects.get( id=target_id, plot_search=instance) PlotSearchTargetCreateUpdateSerializer().update( plot_search_target, target) else: plot_search_target = PlotSearchTargetCreateUpdateSerializer( ).create(target) exist_target_ids.append(plot_search_target.id) PlotSearchTarget.objects.filter(plot_search=instance).exclude( id__in=exist_target_ids).delete() def update(self, instance, validated_data): targets = validated_data.pop("plot_search_targets", None) subtype = validated_data.pop("subtype", None) stage = validated_data.pop("stage", None) preparer = validated_data.pop("preparer", None) if subtype: validated_data["subtype"] = self.dict_to_instance( subtype, PlotSearchSubtype) if stage: validated_data["stage"] = self.dict_to_instance( stage, PlotSearchStage) if preparer: validated_data["preparer"] = self.dict_to_instance(preparer, User) instance = super(PlotSearchUpdateSerializer, self).update(instance, validated_data) if targets is not None: self.handle_targets(targets, instance) return instance
class InvoiceCreateSerializer( UpdateNestedMixin, EnumSupportSerializerMixin, FieldPermissionsSerializerMixin, serializers.ModelSerializer, ): id = serializers.ReadOnlyField() recipient = InstanceDictPrimaryKeyRelatedField( instance_class=Contact, queryset=Contact.objects.all(), related_serializer=ContactSerializer, required=False, ) tenant = InstanceDictPrimaryKeyRelatedField( instance_class=Tenant, queryset=Tenant.objects.all(), related_serializer=TenantSerializer, required=False, ) rows = InvoiceRowCreateUpdateSerializer(many=True) payments = InvoicePaymentCreateUpdateSerializer( many=True, required=False, allow_null=True ) # Make total_amount, billed_amount, and type not required in the serializer and set them in create() if needed total_amount = serializers.DecimalField( max_digits=10, decimal_places=2, required=False ) billed_amount = serializers.DecimalField( max_digits=10, decimal_places=2, required=False ) type = EnumField(enum=InvoiceType, required=False) def override_permission_check_field_name(self, field_name): if field_name == "tenant": return "recipient" return field_name def validate(self, attrs): if not bool(attrs.get("recipient")) ^ bool(attrs.get("tenant")): raise ValidationError(_("Either recipient or tenant is required.")) if ( attrs.get("tenant") and attrs.get("tenant") not in attrs.get("lease").tenants.all() ): raise ValidationError(_("Tenant not found in lease")) return attrs def create(self, validated_data): validated_data["state"] = InvoiceState.OPEN if not validated_data.get("total_amount"): total_amount = Decimal(0) for row in validated_data.get("rows", []): total_amount += row.get("amount", Decimal(0)) validated_data["total_amount"] = total_amount if not validated_data.get("billed_amount"): billed_amount = Decimal(0) for row in validated_data.get("rows", []): billed_amount += row.get("amount", Decimal(0)) validated_data["billed_amount"] = billed_amount if not validated_data.get("type"): validated_data["type"] = InvoiceType.CHARGE if validated_data.get("tenant"): today = datetime.date.today() tenant = validated_data.pop("tenant") billing_tenantcontact = tenant.get_billing_tenantcontacts( start_date=today, end_date=None ).first() if not billing_tenantcontact: raise ValidationError(_("Billing contact not found for tenant")) validated_data["recipient"] = billing_tenantcontact.contact for row in validated_data.get("rows", []): row["tenant"] = tenant invoice = super().create(validated_data) invoice.invoicing_date = timezone.now().date() invoice.outstanding_amount = validated_data["total_amount"] invoice.update_amounts() # 0€ invoice would stay OPEN otherwise invoice.save() return invoice class Meta: model = Invoice exclude = ("deleted",) read_only_fields = ( "number", "generated", "sent_to_sap_at", "sap_id", "state", "adjusted_due_date", "credit_invoices", "interest_invoices", )
class InvoiceRowCreateUpdateSerializer( FieldPermissionsSerializerMixin, serializers.ModelSerializer ): id = serializers.IntegerField(required=False) tenant = InstanceDictPrimaryKeyRelatedField( instance_class=Tenant, queryset=Tenant.objects.all(), related_serializer=TenantSerializer, required=False, allow_null=True, ) receivable_type = InstanceDictPrimaryKeyRelatedField( instance_class=ReceivableType, queryset=ReceivableType.objects.all(), related_serializer=ReceivableTypeSerializer, ) class Meta: model = InvoiceRow fields = ( "id", "tenant", "receivable_type", "billing_period_start_date", "billing_period_end_date", "description", "amount", ) def validate(self, data): """Validate that rows with an inactive receivable type cannot be created Saving an existing row should succeed, but creating new rows or changing a rows receivable type to an inactive type should fail.""" valid = True if not data["receivable_type"].is_active: if self.instance: if self.instance.receivable_type != data["receivable_type"]: # We have an existing row but the receivable type wasn't the # same as before valid = False else: # We have data without an instance. # If there is no id it's a new row. if "id" not in data: valid = False else: # Else try to see if the existing row has the same receivable type try: existing_row = InvoiceRow.objects.get(pk=data["id"]) if existing_row.receivable_type != data["receivable_type"]: valid = False except ObjectDoesNotExist: valid = False if not valid: raise ValidationError(_("Cannot use an inactive receivable type")) return data