feat: add Nepali currency field, number to words, and unicode helpers

This commit is contained in:
sankalp committed 2026-01-30 19:50:26 +05:45
1 parent 2fea4d6db1
commit e606a2b490
14 files changed
+347 -12

No files matched your search

+32
View File
@@ -22,6 +22,8 @@ Building software for local requirements comes with unique challenges, from hand
- **📅 BS Date Support**: Model fields for `nepalidate` and `nepalidatetime` objects.
- **🗺️ Regional Locations**: Pre-defined Provinces, Districts, and Municipalities.
- **📱 Phone Validation**: Patterns for local mobile and landline numbers.
- **💰 Currency Formatting**: `NepaliCurrencyField` with automatic Lakhs/Crores comma placement.
- **🔤 Numbers to Words**: Convert digits into Nepali text representation.
- **🔌 Admin Integration**: Automatic setup for datepickers and localized list displays.
- **🚀 API Support**: DRF Serializers and Filtering backends for BS searching and ordering.
- **⚡ Location Chaining**: Address linking via client side JS or server driven HTMX.
@@ -137,6 +139,36 @@ class ProfileFilter(filters.FilterSet):
---
## 💰 Formatting & Helpers
### 1. Currency & Numbers
Use `NepaliCurrencyField` for automatic formatting in the admin and templates.
```python
from django_nepkit import NepaliCurrencyField
class Transaction(models.Model):
amount = NepaliCurrencyField() # Defaults to 19 digits, 2 decimals
```
### 2. Template Filters
Load the tags to use localized formatting in your templates.
```html
{% load nepali %}
<!-- Comma formatting: Rs. 1,12,000.00 -->
<p>{{ 112000 | nepali_currency }}</p>
<!-- Numbers to Words: एक सय तेईस -->
<p>{{ 123 | nepali_words }}</p>
<!-- English to Nepali Digits: १२३ -->
<p>{{ "123" | nepali_unicode }}</p>
```
---
## 🕒 Technical Design
This library is engineered for performance, data integrity, and local compliance.
+2
View File
@@ -6,6 +6,7 @@ from .models import (
ProvinceField,
DistrictField,
MunicipalityField,
NepaliCurrencyField,
)
from .admin import (
NepaliDateFilter,
@@ -30,4 +31,5 @@ __all__ = [
"format_nepali_datetime",
"NepaliModelAdmin",
"NepaliAdminMixin",
"NepaliCurrencyField",
]
+15 -1
View File
@@ -3,10 +3,11 @@ from django.utils.translation import gettext_lazy as _
from nepali.datetime import nepalidate, nepalidatetime
from django_nepkit.conf import nepkit_settings
from django_nepkit.models import NepaliDateField, NepaliDateTimeField
from django_nepkit.models import NepaliDateField, NepaliDateTimeField, NepaliCurrencyField
from django_nepkit.utils import (
try_parse_nepali_date,
try_parse_nepali_datetime,
format_nepali_currency,
)
from django_nepkit.utils import BS_DATE_FORMAT
@@ -226,6 +227,13 @@ class NepaliAdminMixin:
return format_nepali_datetime(datetime_value, format_string, ne=ne)
def format_nepali_currency(self, value, currency_symbol="Rs.", **kwargs):
"""
Format a number with Nepali-style commas.
Available as a method on admin classes using this mixin.
"""
return format_nepali_currency(value, currency_symbol=currency_symbol)
class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
"""
@@ -272,6 +280,9 @@ class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
def _make_nepali_datetime_display(self, field_name):
return self._make_nepali_display(field_name, self.format_nepali_datetime)
def _make_nepali_currency_display(self, field_name):
return self._make_nepali_display(field_name, self.format_nepali_currency)
def get_list_display(self, request):
list_display = super().get_list_display(request)
result = []
@@ -287,6 +298,9 @@ class NepaliModelAdmin(NepaliAdminMixin, admin.ModelAdmin):
if isinstance(field, NepaliDateTimeField):
result.append(self._make_nepali_datetime_display(item))
continue
if isinstance(field, NepaliCurrencyField):
result.append(self._make_nepali_currency_display(item))
continue
except Exception:
pass
result.append(item)
+9
View File
@@ -239,3 +239,12 @@ class MunicipalityField(BaseLocationField):
description = _("Nepali Municipality")
source = municipalities
widget_class = MunicipalitySelectWidget
class NepaliCurrencyField(models.DecimalField):
description = _("Nepali Currency")
def __init__(self, *args, **kwargs):
kwargs.setdefault("max_digits", 19)
kwargs.setdefault("decimal_places", 2)
super().__init__(*args, **kwargs)
+21
View File
@@ -66,3 +66,24 @@ def nepali_humanize(value, threshold=None, format_str=None):
kwargs["format"] = format_str
return nepalihumanize(value, **kwargs)
@register.filter
def nepali_currency(value, currency_symbol="Rs."):
"""Format a number with Nepali-style commas and optional currency symbol."""
from django_nepkit.utils import format_nepali_currency
return format_nepali_currency(value, currency_symbol=currency_symbol)
@register.filter
def nepali_words(value):
"""Convert a number to Nepali words."""
from django_nepkit.utils import number_to_nepali_words
return number_to_nepali_words(value)
@register.filter
def nepali_unicode(value):
"""Convert English digits to Nepali Unicode."""
from django_nepkit.utils import english_to_nepali_unicode
return english_to_nepali_unicode(value)
@@ -0,0 +1,24 @@
import pytest
from django_nepkit.admin import NepaliModelAdmin
from django_nepkit.models import NepaliCurrencyField
from django.db import models
from django.contrib.admin.sites import AdminSite
class MockModel(models.Model):
amount = NepaliCurrencyField()
class Meta:
app_label = 'django_nepkit'
class MockAdmin(NepaliModelAdmin):
list_display = ('amount',)
def test_admin_currency_formatting():
admin = MockAdmin(MockModel, AdminSite())
displays = admin.get_list_display(None)
# The first item should now be a callable (the display function)
assert callable(displays[0])
# Test the formatter directly
obj = MockModel(amount=1234567)
assert admin.format_nepali_currency(obj.amount) == "Rs. 12,34,567"
@@ -0,0 +1,38 @@
import pytest
from django_nepkit.utils import format_nepali_currency, number_to_nepali_words, english_to_nepali_unicode
from django_nepkit.models import NepaliCurrencyField
from django.db import models
def test_format_nepali_currency():
assert format_nepali_currency(1234567) == "Rs. 12,34,567.00"
assert format_nepali_currency(112000, currency_symbol="") == "1,12,000.00"
assert format_nepali_currency(1234567, currency_symbol="") == "12,34,567.00"
assert format_nepali_currency(100.5) == "Rs. 100.50"
assert format_nepali_currency(None) == ""
def test_number_to_nepali_words():
assert number_to_nepali_words(0) == "शून्य"
assert number_to_nepali_words(1) == "एक"
assert number_to_nepali_words(10) == "दश"
assert number_to_nepali_words(25) == "पच्चीस"
assert number_to_nepali_words(100) == "एक सय"
assert number_to_nepali_words(123) == "एक सय तेईस"
assert number_to_nepali_words(1000) == "एक हजार"
assert number_to_nepali_words(1234) == "एक हजार दुई सय चौंतीस"
assert number_to_nepali_words(100000) == "एक लाख"
assert number_to_nepali_words(1234567) == "बाह्र लाख चौंतीस हजार पाँच सय सतसट्ठी"
# Note: 12,34,567 -> 12 Lakhs 34 Thousand 5 Hundred 67
def test_english_to_nepali_unicode():
assert english_to_nepali_unicode("123") == "१२३"
assert english_to_nepali_unicode("Price: 100") == "Price: १००"
assert english_to_nepali_unicode(None) == ""
def test_nepali_currency_field():
field = NepaliCurrencyField(max_digits=10, decimal_places=2)
assert field.max_digits == 10
assert field.decimal_places == 2
default_field = NepaliCurrencyField()
assert default_field.max_digits == 19
assert default_field.decimal_places == 2
+112
View File
@@ -87,3 +87,115 @@ def get_districts_by_province(province_name, ne=False, en=True):
def get_municipalities_by_district(district_name, ne=False, en=True):
"""Get all municipalities for a district."""
return _get_location_children(districts, district_name, "municipalities", ne=ne)
def format_nepali_currency(number: Any, currency_symbol: str = "Rs.") -> str:
"""
Formats a number with Nepali-style commas and optional currency symbol.
Eg. 1234567 -> Rs. 12,34,567
"""
from nepali.number import add_comma
if number is None:
return ""
try:
# Convert to string and split by decimal point
num_str = f"{float(number):.2f}"
if "." in num_str:
integer_part, decimal_part = num_str.split(".")
else:
integer_part, decimal_part = num_str, ""
# Format integer part with commas
formatted_integer = add_comma(int(integer_part))
# Join back
res = formatted_integer
if decimal_part:
res = f"{res}.{decimal_part}"
if currency_symbol:
return f"{currency_symbol} {res}"
return res
except Exception:
return str(number)
def number_to_nepali_words(number: Any) -> str:
"""
Converts a number to Nepali words.
Eg. 123 -> एक सय तेईस
"""
if number is None:
return ""
# Basic implementation for now, can be expanded
# Mapping for numbers to words (simplified)
# This is a complex task for a full implementation,
# but I'll provide a robust enough version for common usage.
try:
num = int(float(number))
except (ValueError, TypeError):
return str(number)
if num == 0:
return "शून्य"
ones = ["", "एक", "दुई", "तीन", "चार", "पाँच", "छ", "सात", "आठ", "नौ", "दश",
"एघार", "बाह्र", "तेह्र", "चौध", "पन्ध्र", "सोह्र", "सत्र", "अठार", "उन्नाइस", "बीस",
"एकाइस", "बाइस", "तेईस", "चौबीस", "पच्चीस", "छब्बीस", "सत्ताइस", "अठाइस", "उनन्तीस", "तीस",
"एकतीस", "बत्तीस", "तेत्तीस", "चौंतीस", "पैंतीस", "छत्तीस", "सैंतीस", "अठतीस", "उनन्चालीस", "चालीस",
"एकचालीस", "बयालीस", "त्रिचालीस", "चवालीस", "पैंतालीस", "छयालीस", "सत्तालीस", "अठचालीस", "उनन्पचास", "पचास",
"एकाउन्न", "बाउन्न", "त्रिपन्न", "चउन्न", "पचपन्न", "छपन्न", "सन्ताउन्न", "अन्ठाउन्न", "उनन्साठी", "साठी",
"एकसाठी", "बासट्ठी", "त्रिसट्ठी", "चौसट्ठी", "पैंसट्ठी", "छयसट्ठी", "सतसट्ठी", "अठसट्ठी", "उनन्सत्तरी", "सत्तरी",
"एकहत्तर", "बाहत्तर", "त्रिहत्तर", "चौरहत्तर", "पचहत्तर", "छयहत्तर", "सतहत्तर", "अठहत्तर", "उनन्असी", "असी",
"एकासी", "बयासी", "त्रियासी", "चौरासी", "पचासी", "छयासी", "सतासी", "अठासी", "उनन्नब्बे", "नब्बे",
"एकानब्बे", "बयानब्बे", "त्रियानब्बे", "चौरानब्बे", "पञ्चानब्बे", "छ्यानब्बे", "सन्तानब्बे", "अन्ठानब्बे", "उनन्सय"]
units = [
("", ""),
(100, "सय"),
(1000, "हजार"),
(100000, "लाख"),
(10000000, "करोड"),
(1000000000, "अरब"),
(100000000000, "खरब"),
]
def _convert(n):
if n == 0:
return ""
if n < 100:
return ones[n]
for i in range(len(units) - 1, 0, -1):
div, unit_name = units[i]
if n >= div:
prefix_val = n // div
remainder = n % div
# For 'सय' (100), we use ones[prefix_val]
# For others, we might need recursive calls if prefix_val >= 100
prefix_words = _convert(prefix_val)
res = f"{prefix_words} {unit_name}"
if remainder > 0:
res += f" {_convert(remainder)}"
return res.strip()
return ""
return _convert(num)
def english_to_nepali_unicode(text: Any) -> str:
"""
Converts English text/numbers to Nepali Unicode.
Currently focuses on numbers.
"""
from nepali.number import english_to_nepali
if text is None:
return ""
return english_to_nepali(text)
+7 -1
View File
@@ -7,7 +7,7 @@ from django_nepkit import (
NepaliMonthFilter,
)
from .models import Person, Citizen, AuditedPerson
from .models import Person, Citizen, AuditedPerson, Transaction
# Example 1: Basic setup
@@ -58,3 +58,9 @@ class CitizenAdmin(NepaliModelAdmin):
list_display = ("name", "province", "district", "municipality")
# Filters still work with HTMX
list_filter = ("province", "district")
@admin.register(Transaction)
class TransactionAdmin(NepaliModelAdmin):
list_display = ("title", "amount", "date")
list_filter = (("date", NepaliDateFilter),)
+10
View File
@@ -7,6 +7,7 @@ from django_nepkit import (
ProvinceField,
DistrictField,
MunicipalityField,
NepaliCurrencyField,
)
@@ -52,3 +53,12 @@ class AuditedPerson(models.Model):
def __str__(self):
return self.name
class Transaction(models.Model):
title = models.CharField(max_length=100)
amount = NepaliCurrencyField()
date = NepaliDateField(auto_now_add=True)
def __str__(self):
return f"{self.title} - {self.amount}"
+47 -5
View File
@@ -12,7 +12,8 @@
</head>
<body>
<h1>Person List</h1>
<a href="{% url 'demo:person-create' %}">Add New Person</a>
<a href="{% url 'demo:person-create' %}">Add New Person</a> |
<a href="{% url 'demo:transaction-create' %}">Add New Transaction</a>
<hr>
<table>
<thead>
@@ -34,16 +35,16 @@
{% for person in persons %}
<tr>
<td>{{ person.name }}</td>
<td>{{ person.birth_date_ne }}</td>
<td>{{ person.birth_date }}</td>
<td>{{ person.birth_date_en }}</td>
<td>{{ person.registration_time }}</td>
<td>{{ person.phone_number }}</td>
<td>{{ person.province_ne }}</td>
<td>{{ person.province }}</td>
<td>{{ person.province_en }}</td>
<td>{{ person.district_ne }}</td>
<td>{{ person.district }}</td>
<td>{{ person.district_en }}</td>
<td>{{ person.municipality_ne }}</td>
<td>{{ person.municipality }}</td>
<td>{{ person.municipality_en }}</td>
</tr>
{% empty %}
<tr>
@@ -52,5 +53,46 @@
{% endfor %}
</tbody>
</table>
<br><hr>
<h1>Transaction List</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Amount (Raw)</th>
<th>Amount (Formatted)</th>
<th>Amount (Words)</th>
<th>Date</th>
<th>Unicode Example</th>
</tr>
</thead>
<tbody>
{% for tx in transactions %}
<tr>
<td>{{ tx.title }}</td>
<td>{{ tx.amount }}</td>
<td>{{ tx.amount|nepali_currency }}</td>
<td>{{ tx.amount|nepali_words }}</td>
<td>{{ tx.date }}</td>
<td>{{ tx.title|nepali_unicode }}</td>
</tr>
{% empty %}
<tr>
<td colspan="6">No transactions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
<br><hr>
<h2>Quick Helpers Demo</h2>
<ul>
<li><strong>nepali_currency:</strong> 112000 -> {{ 112000|nepali_currency }}</li>
<li><strong>nepali_words:</strong> 1234 -> {{ 1234|nepali_words }}</li>
<li><strong>nepali_unicode:</strong> "Total: 12345" -> {{ "Total: 12345"|nepali_unicode }}</li>
</ul>
</body>
</html>
+1
View File
@@ -12,4 +12,5 @@ router.register("api/audited", views.AuditedPersonViewSet, basename="audited-api
urlpatterns = [
path("", views.person_list, name="person-list"),
path("add/", views.person_create, name="person-create"),
path("transactions/add/", views.transaction_create, name="transaction-create"),
] + router.urls
+26 -2
View File
@@ -3,7 +3,7 @@ from django.shortcuts import render, redirect
from rest_framework import viewsets, filters as drf_filters
from django_filters import rest_framework as django_filters
from django_nepkit.filters import NepaliDateYearFilter, NepaliDateMonthFilter
from .models import Person, Citizen, AuditedPerson
from .models import Person, Citizen, AuditedPerson, Transaction
from .serializers import PersonSerializer, CitizenSerializer, AuditedPersonSerializer
@@ -26,7 +26,12 @@ class PersonForm(forms.ModelForm):
def person_list(request):
persons = Person.objects.all()
return render(request, "demo/person_list.html", {"persons": persons})
transactions = Transaction.objects.all()
return render(
request,
"demo/person_list.html",
{"persons": persons, "transactions": transactions},
)
def person_create(request):
@@ -40,6 +45,25 @@ def person_create(request):
return render(request, "demo/person_form.html", {"form": form})
class TransactionForm(forms.ModelForm):
class Meta:
model = Transaction
fields = ["title", "amount"]
def transaction_create(request):
if request.method == "POST":
form = TransactionForm(request.POST)
if form.is_valid():
form.save()
return redirect("demo:person-list")
else:
form = TransactionForm()
return render(
request, "demo/person_form.html", {"form": form, "title": "Add Transaction"}
)
# --- API Support (DRF) ---
+3 -3
View File
@@ -1,9 +1,9 @@
# Features TODO
## Localization & Formatting
- [ ] **Nepali Currency Formatter**: Template tag for formatting money using the Nepali numbering system (Lakhs, Crores).
- [ ] **Numbers to Words**: Convert digits like `1234` into Nepali text (`एक हजार दुई सय चौंतीस`).
- [ ] **Unicode Helpers**: Built-in filters for converting common English text/numbers into Nepali Unicode equivalent.
- [x] **Nepali Currency Formatter**: Template tag for formatting money using the Nepali numbering system (Lakhs, Crores).
- [x] **Numbers to Words**: Convert digits like `1234` into Nepali text (`एक हजार दुई सय चौंतीस`).
- [x] **Unicode Helpers**: Built-in filters for converting common English text/numbers into Nepali Unicode equivalent.
## Integration
- [ ] **REST API Enhancements**: Add more specialized DRF serializers for range filters and localized outputs.