agdar/finance/forms.py
Marwan Alwali d912313a27 update
2025-11-02 16:03:03 +03:00

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