"""
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')
)