497 lines
19 KiB
Python
497 lines
19 KiB
Python
"""
|
|
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('<hr><h5>{% trans "Line Items" %}</h5>'),
|
|
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')
|
|
)
|