""" Finance forms for the Tenhal Multidisciplinary Healthcare Platform. This module contains forms for invoices, payments, services, and packages. """ from django import forms from django.forms import inlineformset_factory from django.utils.translation import gettext_lazy as _ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Fieldset, Row, Column, Submit, HTML from .models import Invoice, InvoiceLineItem, Payment, Service, Package, PackageService, PackagePurchase, Payer class InvoiceForm(forms.ModelForm): """ Form for creating and editing invoices. """ class Meta: model = Invoice fields = [ 'patient', 'payer', 'issue_date', 'due_date', 'discount', 'tax', 'notes', ] widgets = { 'patient': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select patient'}), 'payer': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select payer'}), 'issue_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'due_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'discount': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}), 'tax': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}), 'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('Optional notes')}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add CSS classes to all fields for field_name, field in self.fields.items(): if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-control' self.helper = FormHelper() self.helper.form_method = 'post' self.helper.layout = Layout( Fieldset( _('Invoice Information'), Row( Column('patient', css_class='form-group col-md-6 mb-0'), Column('payer', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), Row( Column('issue_date', css_class='form-group col-md-6 mb-0'), Column('due_date', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), Row( Column('discount', css_class='form-group col-md-6 mb-0'), Column('tax', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), 'notes', ), HTML('
{% trans "Line Items" %}
'), Submit('submit', _('Save Invoice'), css_class='btn btn-primary') ) class InvoiceLineItemForm(forms.ModelForm): """ Form for invoice line items. """ class Meta: model = InvoiceLineItem fields = ['service', 'package', 'description', 'quantity'] widgets = { 'description': forms.TextInput(attrs={ 'placeholder': _('Optional description'), 'class': 'form-control' }), 'service': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select service'}), 'package': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select package'}), 'quantity': forms.NumberInput(attrs={'class': 'form-control', 'min': '1', 'step': '1', 'value': '1'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Make service and package not required since we'll handle validation self.fields['service'].required = False self.fields['package'].required = False # Add CSS classes for field_name, field in self.fields.items(): if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-control' # Inline formset for invoice line items InvoiceLineItemFormSet = inlineformset_factory( Invoice, InvoiceLineItem, form=InvoiceLineItemForm, extra=0, can_delete=True, min_num=1, validate_min=True, ) class PaymentForm(forms.ModelForm): """ Form for recording payments. """ class Meta: model = Payment fields = [ 'invoice', 'amount', 'method', 'payment_date', 'transaction_id', 'notes', ] widgets = { 'invoice': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select invoice'}), 'method': forms.Select(attrs={'class': 'form-control'}), 'payment_date': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}), 'transaction_id': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Optional')}), 'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('Optional notes')}), } def __init__(self, *args, **kwargs): # Extract invoice_id if provided invoice_id = kwargs.pop('invoice_id', None) super().__init__(*args, **kwargs) # If invoice_id is provided, set it as initial value and make field read-only if invoice_id: self.fields['invoice'].initial = invoice_id self.fields['invoice'].disabled = True self.fields['invoice'].widget.attrs['readonly'] = True # Also set the instance if creating new payment if not self.instance.pk: from .models import Invoice try: self.instance.invoice = Invoice.objects.get(pk=invoice_id) except Invoice.DoesNotExist: pass # Add CSS classes to all fields for field_name, field in self.fields.items(): if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-control' self.helper = FormHelper() self.helper.form_method = 'post' # Conditionally include invoice field in layout if invoice_id: # Hide invoice field when pre-selected layout_fields = [ Row( Column('amount', css_class='form-group col-md-6 mb-0'), Column('method', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), Row( Column('payment_date', css_class='form-group col-md-6 mb-0'), Column('transaction_id', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), 'notes', ] else: # Show invoice field when not pre-selected layout_fields = [ 'invoice', Row( Column('amount', css_class='form-group col-md-6 mb-0'), Column('method', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), Row( Column('payment_date', css_class='form-group col-md-6 mb-0'), Column('transaction_id', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), 'notes', ] self.helper.layout = Layout( Fieldset(_('Payment Details'), *layout_fields), Submit('submit', _('Record Payment'), css_class='btn btn-success') ) def clean(self): cleaned_data = super().clean() invoice = cleaned_data.get('invoice') amount = cleaned_data.get('amount') if invoice and amount: remaining = invoice.amount_due if amount > remaining: raise forms.ValidationError( _('Payment amount (%(amount)s) exceeds remaining balance (%(remaining)s)') % { 'amount': amount, 'remaining': remaining } ) return cleaned_data class ServiceForm(forms.ModelForm): """ Form for managing services. """ class Meta: model = Service fields = [ 'name_en', 'name_ar', 'code', 'clinic', 'base_price', 'duration_minutes', 'description', 'is_active', ] widgets = { 'name_en': forms.TextInput(attrs={'class': 'form-control'}), 'name_ar': forms.TextInput(attrs={'class': 'form-control'}), 'code': forms.TextInput(attrs={'class': 'form-control'}), 'clinic': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select clinic'}), 'base_price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}), 'duration_minutes': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add CSS classes to all fields for field_name, field in self.fields.items(): if field_name == 'is_active': if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-check-input' else: if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-control' self.helper = FormHelper() self.helper.form_method = 'post' self.helper.layout = Layout( Fieldset( _('Service Information'), Row( Column('name_en', css_class='form-group col-md-6 mb-0'), Column('name_ar', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), Row( Column('code', css_class='form-group col-md-4 mb-0'), Column('clinic', css_class='form-group col-md-4 mb-0'), Column('is_active', css_class='form-group col-md-4 mb-0'), css_class='form-row' ), Row( Column('base_price', css_class='form-group col-md-6 mb-0'), Column('duration_minutes', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), 'description', ), Submit('submit', _('Save Service'), css_class='btn btn-primary') ) class PackageForm(forms.ModelForm): """ Form for creating service packages. """ class Meta: model = Package fields = [ 'name_en', 'name_ar', 'price', 'validity_days', 'description', 'is_active', ] widgets = { 'name_en': forms.TextInput(attrs={'class': 'form-control'}), 'name_ar': forms.TextInput(attrs={'class': 'form-control'}), 'price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}), 'validity_days': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}), 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add CSS classes to all fields for field_name, field in self.fields.items(): if field_name == 'is_active': if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-check-input' else: if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-control' class PackageServiceForm(forms.ModelForm): """ Form for package service items (service + session count). """ class Meta: model = PackageService fields = ['service', 'sessions'] widgets = { 'service': forms.Select(attrs={'class': 'form-control select2', 'data-placeholder': 'Select service'}), 'sessions': forms.NumberInput(attrs={'class': 'form-control', 'min': '1', 'value': '1'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Filter to show only active services self.fields['service'].queryset = Service.objects.filter(is_active=True) # Add CSS classes for field_name, field in self.fields.items(): if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-control' # Inline formset for package services PackageServiceFormSet = inlineformset_factory( Package, PackageService, form=PackageServiceForm, extra=0, # Start with 1 empty form can_delete=True, min_num=1, validate_min=True, ) class PackagePurchaseForm(forms.ModelForm): """ Form for purchasing packages. """ class Meta: model = PackagePurchase fields = ['patient', 'package', 'purchase_date', 'expiry_date'] widgets = { 'patient': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select patient'}), 'package': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select package'}), 'purchase_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), 'expiry_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add CSS classes to all fields for field_name, field in self.fields.items(): if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-control' self.helper = FormHelper() self.helper.form_method = 'post' self.helper.layout = Layout( 'patient', 'package', Row( Column('purchase_date', css_class='form-group col-md-6 mb-0'), Column('expiry_date', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), Submit('submit', _('Purchase Package'), css_class='btn btn-success') ) class PayerForm(forms.ModelForm): """ Form for managing insurance payers. """ class Meta: model = Payer fields = [ 'patient', 'name', 'payer_type', 'policy_number', 'coverage_percentage', 'is_active', 'notes', ] widgets = { 'patient': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select patient'}), 'name': forms.TextInput(attrs={'class': 'form-control'}), 'payer_type': forms.Select(attrs={'class': 'form-control'}), 'policy_number': forms.TextInput(attrs={'class': 'form-control'}), 'coverage_percentage': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '100', 'step': '0.01'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'notes': forms.Textarea(attrs={'rows': 2, 'class': 'form-control'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add CSS classes to all fields for field_name, field in self.fields.items(): if field_name == 'is_active': if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-check-input' else: if 'class' not in field.widget.attrs: field.widget.attrs['class'] = 'form-control' self.helper = FormHelper() self.helper.form_method = 'post' self.helper.layout = Layout( Fieldset( _('Payer Information'), 'patient', Row( Column('name', css_class='form-group col-md-6 mb-0'), Column('payer_type', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), Row( Column('policy_number', css_class='form-group col-md-6 mb-0'), Column('coverage_percentage', css_class='form-group col-md-6 mb-0'), css_class='form-row' ), 'is_active', 'notes', ), Submit('submit', _('Save Payer'), css_class='btn btn-primary') ) class InvoiceSearchForm(forms.Form): """ Form for searching invoices. """ search_query = forms.CharField( required=False, label=_('Search'), widget=forms.TextInput(attrs={ 'placeholder': _('Invoice #, Patient name, MRN...'), 'class': 'form-control' }) ) status = forms.ChoiceField( required=False, label=_('Status'), choices=[('', _('All'))] + list(Invoice.Status.choices), widget=forms.Select(attrs={'class': 'form-control'}) ) payer = forms.ModelChoiceField( required=False, label=_('Payer'), queryset=None, # Will be set in __init__ widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select payer'}) ) date_from = forms.DateField( required=False, label=_('From Date'), widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}) ) date_to = forms.DateField( required=False, label=_('To Date'), widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}) ) def __init__(self, *args, **kwargs): tenant = kwargs.pop('tenant', None) super().__init__(*args, **kwargs) if tenant: self.fields['payer'].queryset = Payer.objects.filter(tenant=tenant, is_active=True) self.helper = FormHelper() self.helper.form_method = 'get' self.helper.layout = Layout( Row( Column('search_query', css_class='form-group col-md-4 mb-0'), Column('status', css_class='form-group col-md-2 mb-0'), Column('payer', css_class='form-group col-md-2 mb-0'), Column('date_from', css_class='form-group col-md-2 mb-0'), Column('date_to', css_class='form-group col-md-2 mb-0'), css_class='form-row' ), Submit('search', _('Search'), css_class='btn btn-primary') )