first commit

This commit is contained in:
aminhashemi92 2025-08-10 07:44:23 +03:30
commit b71ea45681
898 changed files with 138202 additions and 0 deletions

286
.gitignore vendored Normal file
View file

@ -0,0 +1,286 @@
.idea
# Created by https://www.toptal.com/developers/gitignore/api/django,python,virtualenv
# Edit at https://www.toptal.com/developers/gitignore?templates=django,python,virtualenv
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
*.sqlite3
db.sqlite3
db.sqlite3-journal
media
#static
staticfiles
profile_images
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/
### Django.Python Stack ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
#dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
# Django stuff:
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs
# Unit test / coverage reports
# Translations
# Django stuff:
# Flask stuff:
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# Jupyter Notebook
# IPython
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
# Celery stuff
# SageMath parsed files
# Environments
# Spyder project settings
# Rope project settings
# mkdocs documentation
# mypy
# Pyre type checker
# pytype static type analyzer
# Cython debug symbols
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### VirtualEnv ###
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
# End of https://www.toptal.com/developers/gitignore/api/django,python,virtualenv
.cursor

0
_base/__init__.py Normal file
View file

16
_base/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for _base project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '_base.settings')
application = get_asgi_application()

172
_base/settings.py Normal file
View file

@ -0,0 +1,172 @@
"""
Django settings for _base project.
Generated by 'django-admin startproject' using Django 5.2.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-h!2hx$h=f6ktgdks!g2_*pg_s1nnuyk+j2yd*_x8r+3+3iyfy*'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
# ------ theme ------ #
'jazzmin',
# ------------------- #
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
# ------- third party apps ------- #
'simple_history',
# -------------------------------- #
# ------- my apps ------- #
'accounts.apps.AccountsConfig',
'locations.apps.LocationsConfig',
'wells.apps.WellsConfig',
'common.apps.CommonConfig',
'processes.apps.ProcessesConfig',
'invoices.apps.InvoicesConfig',
# ----------------------- #
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
]
ROOT_URLCONF = '_base.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'common.context_processors.current_year',
],
},
},
]
WSGI_APPLICATION = '_base.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGES = [
('fa', 'Persian'),
# ('en', 'English')
]
LANGUAGE_CODE = 'fa-ir'
TIME_ZONE = 'Asia/Tehran'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_ROOT = 'ss'
STATIC_URL = 'static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
JAZZMIN_SETTINGS = {
# title of the window (Will default to current_admin_site.site_title if absent or None)
"site_title": "سامانه شفافیت",
# Title on the login screen (19 chars max) (defaults to current_admin_site.site_header if absent or None)
"site_header": "سامانه شفافیت",
# Title on the brand (19 chars max) (defaults to current_admin_site.site_header if absent or None)
"site_brand": "سامانه شفافیت",
# Welcome text on the login screen
"welcome_sign": "به سامانه شفافیت خوش آمدید",
# Copyright on the footer
"copyright": "سامانه شفافیت",
# Logo to use for your site, must be present in static files, used for brand on top left
"site_logo": "../static/dist/img/iconlogo.png",
# Relative paths to custom CSS/JS scripts (must be present in static files)
"custom_css": "../static/admin/css/custom_rtl.css",
"custom_js": None,
}

31
_base/urls.py Normal file
View file

@ -0,0 +1,31 @@
"""
URL configuration for _base project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('accounts.urls')),
path('wells/', include('wells.urls')),
path('processes/', include('processes.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
_base/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for _base project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '_base.settings')
application = get_wsgi_application()

0
_helpers/__init__.py Normal file
View file

192
_helpers/jalali.py Normal file
View file

@ -0,0 +1,192 @@
# In The Name Of Allah
#
# Jalali date converter
# 2014 07 25
# Ported from PHP (http://jdf.scr.ir/) to Python (2&3) by Mohammad Javad Naderi <mjnaderi@gmail.com>
#
# As mentioned in http://jdf.scr.ir/, the original code is free and open source,
# and you are not allowed to sell it. You can read more in http://jdf.scr.ir/.
#
# Original License Notes:
#
# /** Software Hijri_Shamsi , Solar(Jalali) Date and Time
# Copyright(C)2011, Reza Gholampanahi , http://jdf.scr.ir
# version 2.55 :: 1391/08/24 = 1433/12/18 = 2012/11/15 */
#
# /** Convertor from and to Gregorian and Jalali (Hijri_Shamsi,Solar) Functions
# Copyright(C)2011, Reza Gholampanahi [ http://jdf.scr.ir/jdf ] version 2.50 */
#
# Example Usage:
#
# >>> import jalali
#
# >>> jalali.Persian('1393-1-11').gregorian_string()
# '2014-3-31'
# >>> jalali.Persian(1393, 1, 11).gregorian_datetime()
# datetime.date(2014, 3, 31)
# >>> jalali.Persian('1393/1/11').gregorian_string("{}/{}/{}")
# '2014/3/31'
# >>> jalali.Persian((1393, 1, 11)).gregorian_tuple()
# (2014, 3, 31)
#
# >>> jalali.Gregorian('2014-3-31').persian_string()
# '1393-1-11'
# >>> jalali.Gregorian('2014,03,31').persian_tuple()
# (1393, 1, 11)
# >>> jalali.Gregorian(2014, 3, 31).persian_year
# 1393
import re
import datetime
class Gregorian:
def __init__(self, *date):
# Parse date
if len(date) == 1:
date = date[0]
if type(date) is str:
m = re.match(r'^(\d{4})\D(\d{1,2})\D(\d{1,2})$', date)
if m:
[year, month, day] = [int(m.group(1)), int(m.group(2)), int(m.group(3))]
else:
raise Exception("Invalid Input String")
elif type(date) is datetime.date:
[year, month, day] = [date.year, date.month, date.day]
elif type(date) is tuple:
year, month, day = date
year = int(year)
month = int(month)
day = int(day)
else:
raise Exception("Invalid Input Type")
elif len(date) == 3:
year = int(date[0])
month = int(date[1])
day = int(date[2])
else:
raise Exception("Invalid Input")
# Check the validity of input date
try:
datetime.datetime(year, month, day)
except:
raise Exception("Invalid Date")
self.gregorian_year = year
self.gregorian_month = month
self.gregorian_day = day
# Convert date to Jalali
d_4 = year % 4
g_a = [0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
doy_g = g_a[month] + day
if d_4 == 0 and month > 2:
doy_g += 1
d_33 = int(((year - 16) % 132) * .0305)
a = 286 if (d_33 == 3 or d_33 < (d_4 - 1) or d_4 == 0) else 287
if (d_33 == 1 or d_33 == 2) and (d_33 == d_4 or d_4 == 1):
b = 78
else:
b = 80 if (d_33 == 3 and d_4 == 0) else 79
if int((year - 10) / 63) == 30:
a -= 1
b += 1
if doy_g > b:
jy = year - 621
doy_j = doy_g - b
else:
jy = year - 622
doy_j = doy_g + a
if doy_j < 187:
jm = int((doy_j - 1) / 31)
jd = doy_j - (31 * jm)
jm += 1
else:
jm = int((doy_j - 187) / 30)
jd = doy_j - 186 - (jm * 30)
jm += 7
self.persian_year = jy
self.persian_month = jm
self.persian_day = jd
def persian_tuple(self):
return self.persian_year, self.persian_month, self.persian_day
def persian_string(self, date_format="{}-{}-{}"):
return date_format.format(self.persian_year, self.persian_month, self.persian_day)
class Persian:
def __init__(self, *date):
# Parse date
if len(date) == 1:
date = date[0]
if type(date) is str:
m = re.match(r'^(\d{4})\D(\d{1,2})\D(\d{1,2})$', date)
if m:
[year, month, day] = [int(m.group(1)), int(m.group(2)), int(m.group(3))]
else:
raise Exception("Invalid Input String")
elif type(date) is tuple:
year, month, day = date
year = int(year)
month = int(month)
day = int(day)
else:
raise Exception("Invalid Input Type")
elif len(date) == 3:
year = int(date[0])
month = int(date[1])
day = int(date[2])
else:
raise Exception("Invalid Input")
# Check validity of date. TODO better check (leap years)
if year < 1 or month < 1 or month > 12 or day < 1 or day > 31 or (month > 6 and day == 31):
raise Exception("Incorrect Date")
self.persian_year = year
self.persian_month = month
self.persian_day = day
# Convert date
d_4 = (year + 1) % 4
if month < 7:
doy_j = ((month - 1) * 31) + day
else:
doy_j = ((month - 7) * 30) + day + 186
d_33 = int(((year - 55) % 132) * .0305)
a = 287 if (d_33 != 3 and d_4 <= d_33) else 286
if (d_33 == 1 or d_33 == 2) and (d_33 == d_4 or d_4 == 1):
b = 78
else:
b = 80 if (d_33 == 3 and d_4 == 0) else 79
if int((year - 19) / 63) == 20:
a -= 1
b += 1
if doy_j <= a:
gy = year + 621
gd = doy_j + b
else:
gy = year + 622
gd = doy_j - a
for gm, v in enumerate([0, 31, 29 if (gy % 4 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]):
if gd <= v:
break
gd -= v
self.gregorian_year = gy
self.gregorian_month = gm
self.gregorian_day = gd
def gregorian_tuple(self):
return self.gregorian_year, self.gregorian_month, self.gregorian_day
def gregorian_string(self, date_format="{}-{}-{}"):
return date_format.format(self.gregorian_year, self.gregorian_month, self.gregorian_day)
def gregorian_datetime(self):
return datetime.date(self.gregorian_year, self.gregorian_month, self.gregorian_day)

195
_helpers/utils.py Normal file
View file

@ -0,0 +1,195 @@
import datetime
import os
import uuid
from django.core.validators import RegexValidator
from django.utils import timezone
from django.utils.text import slugify
from . import jalali
__jmonths = [
"فروردین", "اردیبهشت", "خرداد",
"تیر", "مرداد", "شهریور",
"مهر", "آبان", "آذر",
"دی", "بهمن", "اسفند",
]
def persian_numbers_converter(mystr):
numbers = {
"0": "۰",
"1": "۱",
"2": "۲",
"3": "۳",
"4": "۴",
"5": "۵",
"6": "۶",
"7": "۷",
"8": "۸",
"9": "۹",
}
for e, p in numbers.items():
mystr = mystr.replace(e, p)
return mystr
def jalali_converter(time):
try:
time = timezone.localtime(time)
except:
time = time
time_to_str = "{},{},{}".format(time.year, time.month, time.day)
time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
time_to_list = list(time_to_tuple)
for index, month in enumerate(__jmonths):
if time_to_list[1] == index + 1:
time_to_list[1] = month
break
output = "{} {} {}, ساعت {}:{}".format(
time_to_list[2],
time_to_list[1],
time_to_list[0],
time.hour,
time.minute,
)
return persian_numbers_converter(output)
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def jalali_converter21(time):
time = timezone.localtime(time)
year, month, day = jalali.Gregorian(time.strftime("%Y-%m-%d")).persian_tuple()[:3]
hour, minute = time.strftime("%H:%M").split(':')
return f"{day} {__jmonths[month - 1]} {year}, ساعت {hour}:{minute}"
def jalali_converter2(time):
try:
time = timezone.localdate(time)
except:
time = time
time_to_str = "{},{},{}".format(time.year, time.month, time.day)
time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
time_to_list = list(time_to_tuple)
for index, month in enumerate(__jmonths):
if time_to_list[1] == index + 1:
time_to_list[1] = month
break
output = "{} {} {}".format(
time_to_list[2],
time_to_list[1],
time_to_list[0],
)
return persian_numbers_converter(output)
def gregorian_converter(time):
time_to_list = time.split('/')
time_to_str = "{},{},{}".format(time_to_list[0], time_to_list[1], time_to_list[2])
return jalali.Persian(time_to_str).gregorian_string()
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def persian_converter(time):
time = time + datetime.timedelta(days=1)
time_to_str = "{},{},{}".format(time.year, time.month, time.day)
time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
time_to_list = list(time_to_tuple)
return time_to_list
def persian_converter2(time):
time_to_str = "{},{},{}".format(time.year, time.month, time.day)
time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
time_to_list = list(time_to_tuple)
for index, month in enumerate(__jmonths):
if time_to_list[1] == index + 1:
time_to_list[1] = month
break
output = "{} {} {}".format(
time_to_list[2],
time_to_list[1],
time_to_list[0],
)
return persian_numbers_converter(output)
def persian_converter3(time):
time = time + datetime.timedelta(days=1)
time_to_str = "{},{},{}".format(time.year, time.month, time.day)
time_to_tuple = jalali.Gregorian(time_to_str).persian_tuple()
time_to_list = list(time_to_tuple)
time_to_list = [str(item) for item in time_to_list]
return '/'.join(time_to_list)
def delete_file(path):
""" Deletes file from filesystem. """
if os.path.isfile(path):
os.remove(path)
def get_phone_number_validator():
return RegexValidator(
regex=r'^[0]{1}[9]{1}[0-9]{9}',
message='مقدار وارد شده صحیح نمی‌باشد',
)
def generate_unique_slug(text: str) -> str:
"""
Generates a unique slug from received text
"""
slug = slugify(text)
unique_id = str(uuid.uuid4())[:8]
unique_slug = f"{slug}-{unique_id}"
return unique_slug
def normalize_size(size: int) -> str:
"""
Normalizes file or volume sizes and returns it with the proper suffix,
showing the decimal only if it's non-zero.
"""
size = float(size)
if size < 1024:
return f"{int(size)} B" if size.is_integer() else f"{size:.1f} B"
elif size < 1024 * 1024:
size_kb = size / 1024
return f"{int(size_kb)} KB" if size_kb.is_integer() else f"{size_kb:.1f} KB"
elif size < 1024 * 1024 * 1024:
size_mb = size / (1024 * 1024)
return f"{int(size_mb)} MB" if size_mb.is_integer() else f"{size_mb:.1f} MB"
else:
size_gb = size / (1024 * 1024 * 1024)
return f"{int(size_gb)} GB" if size_gb.is_integer() else f"{size_gb:.1f} GB"

0
accounts/__init__.py Normal file
View file

32
accounts/admin.py Normal file
View file

@ -0,0 +1,32 @@
from django.contrib import admin
from accounts.models import Role, Profile
# Register your models here.
@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'parent', 'is_active']
search_fields = ['name', 'slug']
list_filter = ['is_active']
ordering = ['parent__name', 'name']
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = [
"user",
"fullname",
"pic_tag",
"roles_str",
"affairs",
"county",
"broker",
"is_completed",
"is_active",
"jcreated",
]
search_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__phone_number']
list_filter = ['user', 'roles', 'affairs', 'county', 'broker']
date_hierarchy = 'created'
ordering = ['-created']
readonly_fields = ['created', 'updated']

7
accounts/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
verbose_name = "حساب‌ها"

157
accounts/forms.py Normal file
View file

@ -0,0 +1,157 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
from .models import Profile, Role
from common.consts import UserRoles
User = get_user_model()
class CustomerForm(forms.ModelForm):
"""فرم برای افزودن مشترک جدید"""
first_name = forms.CharField(
max_length=150,
label="نام",
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'نام'
})
)
last_name = forms.CharField(
max_length=150,
label="نام خانوادگی",
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'نام خانوادگی'
})
)
class Meta:
model = Profile
fields = [
'phone_number_1', 'phone_number_2', 'national_code',
'address', 'card_number', 'account_number'
]
widgets = {
'phone_number_1': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '09123456789'
}),
'phone_number_2': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '02112345678'
}),
'national_code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '1234567890',
'maxlength': '10',
'required': 'required'
}),
'address': forms.Textarea(attrs={
'class': 'form-control',
'placeholder': 'آدرس کامل',
'rows': '3'
}),
'card_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'شماره کارت بانکی',
'maxlength': '16'
}),
'account_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'شماره حساب بانکی',
'maxlength': '20'
}),
}
labels = {
'phone_number_1': 'تلفن ۱',
'phone_number_2': 'تلفن ۲',
'national_code': 'کد ملی',
'address': 'آدرس',
'card_number': 'شماره کارت',
'account_number': 'شماره حساب',
}
def clean_national_code(self):
national_code = self.cleaned_data.get('national_code')
if national_code:
# Check if user with this national code exists
existing_user = User.objects.filter(username=national_code).first()
if existing_user:
# If we're editing and the user is the same, it's OK
if self.instance and self.instance.user and existing_user == self.instance.user:
return national_code
# Otherwise, it's a duplicate
raise forms.ValidationError('این کد ملی قبلاً استفاده شده است.')
return national_code
def save(self, commit=True):
# Check if this is an update (instance exists)
if self.instance and self.instance.pk:
# Update existing profile
profile = super().save(commit=False)
# Update user information
user = profile.user
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.save()
# Set affairs, county, and broker from current user's profile
if hasattr(self, 'request') and self.request.user.is_authenticated:
current_user_profile = getattr(self.request.user, 'profile', None)
if current_user_profile:
profile.affairs = current_user_profile.affairs
profile.county = current_user_profile.county
profile.broker = current_user_profile.broker
if commit:
profile.save()
self.save_m2m()
return profile
else:
# Create new profile
# Get national code as username
national_code = self.cleaned_data.get('national_code')
if not national_code:
raise forms.ValidationError('کد ملی الزامی است.')
# Create User with default password
user = User.objects.create_user(
username=national_code,
email='', # Empty email
password='sooha1234', # Default password
first_name=self.cleaned_data['first_name'],
last_name=self.cleaned_data['last_name']
)
# Create Profile
profile = super().save(commit=False)
profile.user = user
profile.owner = user
# Set affairs, county, and broker from current user's profile
if hasattr(self, 'request') and self.request.user.is_authenticated:
current_user_profile = getattr(self.request.user, 'profile', None)
if current_user_profile:
profile.affairs = current_user_profile.affairs
profile.county = current_user_profile.county
profile.broker = current_user_profile.broker
if commit:
profile.save()
self.save_m2m()
# Add customer role after profile is saved
customer_role = Role.objects.filter(slug=UserRoles.CUSTOMER.value).first()
if customer_role:
profile.roles.add(customer_role)
else:
# Create customer role if it doesn't exist
customer_role = Role.objects.create(
name='مشترک',
slug=UserRoles.CUSTOMER.value
)
profile.roles.add(customer_role)
return profile

View file

@ -0,0 +1 @@
# Management package for accounts app

View file

@ -0,0 +1 @@
# Commands package for accounts app

View file

@ -0,0 +1,51 @@
from django.core.management.base import BaseCommand
from accounts.models import Role
from common.consts import UserRoles
class Command(BaseCommand):
help = "Generates default roles"
def handle(self, *args, **options):
roles = [
{
"name": "ادمین",
"slug": UserRoles.ADMIN,
},
{
"name": "مشترک",
"slug": UserRoles.CUSTOMER,
},
{
"name": "مدیر",
"slug": UserRoles.MANAGER,
},
{
"name": "حسابدار",
"slug": UserRoles.ACCOUNTANT,
},
{
"name": "پیشخوان",
"slug": UserRoles.BROKER,
},
{
"name": "نصاب",
"slug": UserRoles.INSTALLER,
},
{
"name": "کارشناس امور",
"slug": UserRoles.REGIONAL_WATER_AUTHORITY,
},
{
"name": "مدیر منابع آب",
"slug": UserRoles.WATER_RESOURCE_MANAGER,
},
{
"name": "ستاد آب‌منطقه‌ای",
"slug": UserRoles.HEADQUARTER,
},
]
for role in roles:
Role.objects.get_or_create(name=role['name'], slug=role['slug'].value)

View file

@ -0,0 +1,62 @@
# Generated by Django 5.2.4 on 2025-08-07 09:08
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Role',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('parent', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='accounts.role', verbose_name='زیرشاخه')),
],
options={
'verbose_name': 'نقش',
'verbose_name_plural': 'نقش\u200cها',
},
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('national_code', models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_national_code', message='کد ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='کد ملی')),
('address', models.TextField(blank=True, null=True, verbose_name='آدرس')),
('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')),
('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')),
('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')),
('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')),
('pic', models.ImageField(default='../static/dist/img/profile.jpg', upload_to='profile_images', verbose_name='تصویر')),
('is_completed', models.BooleanField(default=False, verbose_name='پروفایل تکمیل شده')),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_profiles', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
('roles', models.ManyToManyField(blank=True, related_name='profiles', to='accounts.role', verbose_name='نقش\u200cها')),
],
options={
'verbose_name': 'پروفایل',
'verbose_name_plural': 'پروفایل\u200cها',
},
),
]

View file

@ -0,0 +1,75 @@
# Generated by Django 5.2.4 on 2025-08-07 14:29
import django.core.validators
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
('locations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='profile',
name='affairs',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.affairs', verbose_name='امور'),
),
migrations.AddField(
model_name='profile',
name='broker',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.broker', verbose_name='کارگزار'),
),
migrations.AddField(
model_name='profile',
name='county',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='locations.county', verbose_name='شهرستان'),
),
migrations.AlterField(
model_name='profile',
name='pic',
field=models.ImageField(default='../static/sample_images/profile.jpg', upload_to='profile_images', verbose_name='تصویر'),
),
migrations.CreateModel(
name='HistoricalProfile',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('national_code', models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_national_code', message='کد ملی باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='کد ملی')),
('address', models.TextField(blank=True, null=True, verbose_name='آدرس')),
('card_number', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(code='invalid_card_number', message='شماره کارت باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره کارت')),
('account_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_account_number', message='شماره حساب باید فقط شامل اعداد باشد.', regex='^\\d+$')], verbose_name='شماره حساب')),
('phone_number_1', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۱')),
('phone_number_2', models.CharField(blank=True, max_length=11, null=True, verbose_name='شماره تماس ۲')),
('pic', models.TextField(default='../static/sample_images/profile.jpg', max_length=100, verbose_name='تصویر')),
('is_completed', models.BooleanField(default=False, verbose_name='پروفایل تکمیل شده')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('affairs', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.affairs', verbose_name='امور')),
('broker', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.broker', verbose_name='کارگزار')),
('county', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='locations.county', verbose_name='شهرستان')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')),
],
options={
'verbose_name': 'historical پروفایل',
'verbose_name_plural': 'historical پروفایل\u200cها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]

View file

172
accounts/models.py Normal file
View file

@ -0,0 +1,172 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.html import format_html
from django.core.validators import RegexValidator
from simple_history.models import HistoricalRecords
from common.models import TagModel, BaseModel
from common.consts import UserRoles
from locations.models import Affairs, Broker, County
# Create your models here.
class Role(TagModel):
class Meta:
verbose_name = "نقش"
verbose_name_plural = "نقش‌ها"
class Profile(BaseModel):
user = models.OneToOneField(
get_user_model(),
null=True,
on_delete=models.CASCADE,
verbose_name="کاربر",
related_name="profile"
)
national_code = models.CharField(
max_length=10,
null=True,
verbose_name="کد ملی",
blank=True,
validators=[
RegexValidator(
regex=r'^\d+$',
message='کد ملی باید فقط شامل اعداد باشد.',
code='invalid_national_code'
)
]
)
address = models.TextField(
null=True,
verbose_name="آدرس",
blank=True
)
card_number = models.CharField(
max_length=16,
null=True,
verbose_name="شماره کارت",
blank=True,
validators=[
RegexValidator(
regex=r'^\d+$',
message='شماره کارت باید فقط شامل اعداد باشد.',
code='invalid_card_number'
)
]
)
account_number = models.CharField(
max_length=20,
null=True,
verbose_name="شماره حساب",
blank=True,
validators=[
RegexValidator(
regex=r'^\d+$',
message='شماره حساب باید فقط شامل اعداد باشد.',
code='invalid_account_number'
)
]
)
phone_number_1 = models.CharField(
max_length=11,
null=True,
verbose_name="شماره تماس ۱",
blank=True
)
phone_number_2 = models.CharField(
max_length=11,
null=True,
verbose_name="شماره تماس ۲",
blank=True
)
pic = models.ImageField(
upload_to="profile_images",
verbose_name="تصویر",
default="../static/sample_images/profile.jpg"
)
roles = models.ManyToManyField(
Role,
verbose_name="نقش‌ها",
blank=True,
related_name="profiles"
)
is_completed = models.BooleanField(
default=False,
verbose_name="پروفایل تکمیل شده"
)
owner = models.ForeignKey(
get_user_model(),
null=True,
on_delete=models.SET_NULL,
verbose_name="ایجاد کننده",
blank=True,
related_name="owned_profiles"
)
affairs = models.ForeignKey(
Affairs,
on_delete=models.SET_NULL,
verbose_name="امور",
null=True,
blank=True
)
county = models.ForeignKey(
County,
on_delete=models.SET_NULL,
verbose_name="شهرستان",
null=True,
blank=True
)
broker = models.ForeignKey(
Broker,
on_delete=models.SET_NULL,
verbose_name="کارگزار",
null=True,
blank=True
)
history = HistoricalRecords()
class Meta:
verbose_name = "پروفایل"
verbose_name_plural = "پروفایل‌ها"
def __str__(self):
return str(self.user)
def first_name(self):
return self.user.first_name
first_name.short_description = "نام"
def last_name(self):
return self.user.last_name
last_name.short_description = "نام خانوادگی"
def fullname(self):
return self.user.get_full_name()
fullname.short_description = "نام و نام خانوادگی"
def roles_str(self):
return ', '.join([role.name for role in self.roles.all()])
roles_str.short_description = "نقش‌ها"
def has_role(self, role: UserRoles):
return self.roles.filter(slug=role.value).exists()
def has_any_of(self, roles: list[UserRoles]):
allowed_roles = [role.value for role in roles]
return len(self.roles.filter(slug__in=allowed_roles)) > 0
def has_none_of(self, roles: list[UserRoles]):
return not self.has_any_of(roles)
def pic_tag(self):
return format_html(f"<img style='width:30px;' src='{self.pic.url}'>")
pic_tag.short_description = "تصویر"

View file

@ -0,0 +1,489 @@
{% extends '_base.html' %}
{% load static %}
{% block sidebar %}
{% include 'sidebars/admin.html' %}
{% endblock sidebar %}
{% block navbar %}
{% include 'navbars/admin.html' %}
{% endblock navbar %}
{% block title %}کاربرها{% endblock %}
{% block style %}
<!-- DataTables CSS -->
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-bs5/datatables.bootstrap5.css' %}">
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.css' %}">
<link rel="stylesheet" href="{% static 'assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.css' %}">
{% endblock %}
{% block content %}
<!-- Include Toasts -->
{% include '_toasts.html' %}
<div class="container-xxl flex-grow-1 container-p-y">
<div class="row py-3 mb-4 card-header flex-column flex-md-row pb-0">
<div class="d-md-flex justify-content-between align-items-center dt-layout-start col-md-auto me-auto mt-0">
<h5 class="card-title mb-0 text-md-start text-center fw-bold">لیست کاربرها</h5>
</div>
<div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ms-auto mt-0">
<div class="dt-buttons btn-group flex-wrap mb-0">
<div class="btn-group">
<button class="btn buttons-collection btn-label-primary dropdown-toggle me-4" tabindex="0" aria-controls="DataTables_Table_0" type="button" aria-haspopup="dialog" aria-expanded="false">
<span>
<span class="d-flex align-items-center gap-2">
<i class="icon-base bx bx-export me-sm-1">
</i>
<span class="d-none d-sm-inline-block">
خروجی
</span>
</span>
</span>
</button>
<button class="btn btn-primary " onclick="prepareAddForm()">
<i class="bx bx-plus me-1"></i>
افزودن کاربر جدید
</button>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-datatable table-responsive">
<table class="datatables-basic table border-top" id="customers-table">
<thead>
<tr>
<th>ردیف</th>
<th>کاربر</th>
<th>کد ملی</th>
<th>تلفن</th>
<th>آدرس</th>
<th>وضعیت</th>
<th>عملیات</th>
</tr>
</thead>
<tbody>
{% for customer in customers %}
<tr>
<td>{{forloop.counter}}</td>
<td>
<div class="d-flex justify-content-start align-items-center user-name">
<div class="avatar-wrapper">
<div class="avatar me-2">
{% if customer.pic and customer.pic.url %}
<img src="{{ customer.pic.url }}" alt="Avatar" class="rounded-circle" style="width: 40px; height: 40px; object-fit: cover;">
{% else %}
<span class="avatar-initial rounded-circle bg-label-primary">
{% if customer.user.first_name and customer.user.last_name %}
{{ customer.user.first_name|first|upper }}{{ customer.user.last_name|first|upper }}
{% elif customer.user.first_name %}
{{ customer.user.first_name|first|upper }}
{% elif customer.user.last_name %}
{{ customer.user.last_name|first|upper }}
{% else %}
{{ customer.user.username|first|upper }}
{% endif %}
</span>
{% endif %}
</div>
</div>
<div class="d-flex flex-column">
<span class="emp_name text-truncate text-heading">{{ customer.user.get_full_name|default:customer.user.username }}</span>
<small class="emp_post text-truncate">{{ customer.user.username }}</small>
</div>
</div>
</td>
<td>{{ customer.national_code|default:"کد ملی ثبت نشده" }}</td>
<td>
<div class="d-flex flex-column">
{% if customer.phone_number_1 %}
<span class="fw-medium">{{ customer.phone_number_1 }}</span>
{% endif %}
{% if customer.phone_number_2 %}
<small class="text-muted">{{ customer.phone_number_2 }}</small>
{% endif %}
{% if not customer.phone_number_1 and not customer.phone_number_2 %}
<span class="text-muted">تلفن ثبت نشده</span>
{% endif %}
</div>
</td>
<td>
{% if customer.address %}
<span class="text-truncate d-inline-block" style="max-width: 200px;" title="{{ customer.address }}">
{{ customer.address|truncatechars:50 }}
</span>
{% else %}
<span class="text-muted">آدرس ثبت نشده</span>
{% endif %}
</td>
<td>
{% if customer.is_completed %}
<span class="badge bg-label-success">تکمیل شده</span>
{% else %}
<span class="badge bg-label-warning">ناقص</span>
{% endif %}
</td>
<td>
<div class="d-inline-block">
<a href="javascript:;" class="btn btn-icon dropdown-toggle hide-arrow" data-bs-toggle="dropdown">
<i class="icon-base bx bx-dots-vertical-rounded"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end m-0">
<li>
<a href="#" class="dropdown-item" data-customer-id="{{ customer.id }}" onclick="viewCustomer(this.getAttribute('data-customer-id'))">
<i class="bx bx-show me-1"></i>مشاهده جزئیات
</a>
</li>
<li>
<a href="#" class="dropdown-item" data-customer-id="{{ customer.id }}" onclick="editCustomer(this.getAttribute('data-customer-id'))">
<i class="bx bx-edit me-1"></i>ویرایش
</a>
</li>
<div class="dropdown-divider"></div>
<li>
<a href="#" class="dropdown-item text-danger" data-customer-id="{{ customer.id }}" data-customer-name="{{ customer.user.get_full_name|default:customer.user.username }}" onclick="deleteCustomer(this.getAttribute('data-customer-id'), this.getAttribute('data-customer-name'))">
<i class="bx bx-trash me-1"></i>حذف
</a>
</li>
</ul>
</div>
<a href="#" class="btn btn-icon item-edit" data-customer-id="{{ customer.id }}" onclick="editCustomer(this.getAttribute('data-customer-id'))">
<i class="icon-base bx bx-edit icon-sm"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-4">
<div class="d-flex flex-column align-items-center">
<i class="bx bx-user-x bx-lg text-muted mb-2"></i>
<span class="text-muted">هیچ کاربری یافت نشد</span>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal to add/edit record -->
<div class="offcanvas offcanvas-end" id="add-new-record">
<div class="offcanvas-header border-bottom">
<h5 class="offcanvas-title" id="exampleModalLabel">افزودن کاربر جدید</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body flex-grow-1">
<form class="add-new-record pt-0 row g-2" id="form-add-new-record" method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" id="customer-id" name="customer_id" value="">
<!-- User Information -->
<div class="col-sm-6">
<label class="form-label fw-bold" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-user"></i></span>
{{ form.first_name }}
</div>
{% if form.first_name.errors %}
<div class="invalid-feedback d-block">{{ form.first_name.errors.0 }}</div>
{% endif %}
</div>
<div class="col-sm-6">
<label class="form-label fw-bold" for="{{ form.last_name.id_for_label }}">{{ form.last_name.label }}</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-user"></i></span>
{{ form.last_name }}
</div>
{% if form.last_name.errors %}
<div class="invalid-feedback d-block">{{ form.last_name.errors.0 }}</div>
{% endif %}
</div>
<div class="col-sm-6">
<label class="form-label fw-bold" for="{{ form.phone_number_1.id_for_label }}">{{ form.phone_number_1.label }}</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-phone"></i></span>
{{ form.phone_number_1 }}
</div>
{% if form.phone_number_1.errors %}
<div class="invalid-feedback d-block">{{ form.phone_number_1.errors.0 }}</div>
{% endif %}
</div>
<div class="col-sm-6">
<label class="form-label fw-bold" for="{{ form.phone_number_2.id_for_label }}">{{ form.phone_number_2.label }}</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-phone"></i></span>
{{ form.phone_number_2 }}
</div>
{% if form.phone_number_2.errors %}
<div class="invalid-feedback d-block">{{ form.phone_number_2.errors.0 }}</div>
{% endif %}
</div>
<div class="col-sm-12">
<label class="form-label fw-bold" for="{{ form.national_code.id_for_label }}">{{ form.national_code.label }}</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-id-card"></i></span>
{{ form.national_code }}
</div>
{% if form.national_code.errors %}
<div class="invalid-feedback d-block">{{ form.national_code.errors.0 }}</div>
{% endif %}
</div>
<div class="col-sm-12">
<label class="form-label fw-bold" for="{{ form.card_number.id_for_label }}">{{ form.card_number.label }}</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-credit-card"></i></span>
{{ form.card_number }}
</div>
{% if form.card_number.errors %}
<div class="invalid-feedback d-block">{{ form.card_number.errors.0 }}</div>
{% endif %}
</div>
<div class="col-sm-12">
<label class="form-label fw-bold" for="{{ form.account_number.id_for_label }}">{{ form.account_number.label }}</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-credit-card"></i></span>
{{ form.account_number }}
</div>
{% if form.account_number.errors %}
<div class="invalid-feedback d-block">{{ form.account_number.errors.0 }}</div>
{% endif %}
</div>
<div class="col-sm-12">
<label class="form-label fw-bold" for="{{ form.address.id_for_label }}">{{ form.address.label }}</label>
<div class="input-group input-group-merge">
<span class="input-group-text"><i class="bx bx-map"></i></span>
{{ form.address }}
</div>
{% if form.address.errors %}
<div class="invalid-feedback d-block">{{ form.address.errors.0 }}</div>
{% endif %}
</div>
<div class="col-sm-12">
<button type="submit" class="btn btn-primary data-submit me-sm-3 me-1">ذخیره</button>
<button type="reset" class="btn btn-outline-secondary" data-bs-dismiss="offcanvas">انصراف</button>
</div>
</form>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteConfirmModalLabel">تایید حذف</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p id="deleteConfirmText">آیا از حذف این کاربر اطمینان دارید؟</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">انصراف</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">حذف</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<!-- DataTables JS -->
<script src="{% static 'assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js' %}"></script>
<!-- Persian DataTable Language -->
<script src="{% static 'assets/js/persian-datatable.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable with Persian language
$('#customers-table').DataTable({
pageLength: 10,
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "همه"]],
order: [[0, 'asc']],
responsive: true,
});
// Handle form submission
$('#form-add-new-record').on('submit', function(e) {
e.preventDefault();
const form = this;
const formData = new FormData(form);
const customerId = $('#customer-id').val();
// Determine URL based on whether we're editing or adding
const url = customerId ? '{% url "accounts:edit_customer_ajax" 0 %}'.replace('0', customerId) : '{% url "accounts:add_customer_ajax" %}';
// Show loading state
const submitBtn = $(form).find('button[type="submit"]');
const originalText = submitBtn.text();
submitBtn.prop('disabled', true).text('در حال ذخیره...');
$.ajax({
url: url,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
// Show success message
showToast(response.message, 'success');
// Close offcanvas and reset form
$('#add-new-record').offcanvas('hide');
form.reset();
// Reload page to show new customer
setTimeout(function() {
location.reload();
}, 1500);
} else {
// Show error message
showToast(response.message, 'danger');
// Show form errors if any
if (response.errors) {
Object.keys(response.errors).forEach(function(field) {
const errorMsg = response.errors[field][0];
const fieldElement = $('[name="' + field + '"]');
fieldElement.addClass('is-invalid');
fieldElement.siblings('.invalid-feedback').remove();
fieldElement.after('<div class="invalid-feedback d-block">' + errorMsg + '</div>');
});
}
}
},
error: function(xhr, status, error) {
// Show error message
showToast('خطا در ارتباط با سرور', 'danger');
},
complete: function() {
// Reset button state
submitBtn.prop('disabled', false).text(originalText);
}
});
});
// Reset form when offcanvas is hidden
$('#add-new-record').on('hidden.bs.offcanvas', function() {
const form = $('#form-add-new-record')[0];
form.reset();
// Reset form for adding new customer
$('#customer-id').val('');
$('#exampleModalLabel').text('افزودن کاربر جدید');
$('.data-submit').text('ذخیره');
// Clear validation errors
$('.is-invalid').removeClass('is-invalid');
$('.invalid-feedback').remove();
});
// Clear validation errors when user starts typing
$('input, textarea').on('input', function() {
$(this).removeClass('is-invalid');
$(this).siblings('.invalid-feedback').remove();
});
});
// Customer functions
function viewCustomer(id) {
// Implement view functionality
console.log('View customer:', id);
}
function editCustomer(id) {
// Load customer data and open edit form
$.ajax({
url: '{% url "accounts:get_customer_data" 0 %}'.replace('0', id),
type: 'GET',
success: function(response) {
if (response.success) {
const customer = response.customer;
// Fill form with customer data
const fieldsMap = {
'customer-id': customer.id,
'id_first_name': customer.first_name,
'id_last_name': customer.last_name,
'id_phone_number_1': customer.phone_number_1,
'id_phone_number_2': customer.phone_number_2,
'id_national_code': customer.national_code,
'id_card_number': customer.card_number,
'id_account_number': customer.account_number,
'id_address': customer.address
};
// Loop through fields for easier maintenance
Object.keys(fieldsMap).forEach(function(fieldId) {
$('#' + fieldId).val(fieldsMap[fieldId] || '');
});
// Update modal title and button
$('#exampleModalLabel').text('ویرایش کاربر');
$('.data-submit').text('ویرایش');
// Open modal
$('#add-new-record').offcanvas('show');
} else {
showToast('خطا در بارگذاری اطلاعات کاربر', 'danger');
}
},
error: function() {
showToast('خطا در ارتباط با سرور', 'danger');
}
});
}
function deleteCustomer(id, name) {
// Set modal content
document.getElementById('deleteConfirmText').textContent = `آیا از حذف کاربر "${name}" اطمینان دارید؟`;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
modal.show();
// Handle confirm button click
document.getElementById('confirmDeleteBtn').onclick = function() {
// Implement delete functionality
console.log('Delete customer:', id);
showToast('کاربر با موفقیت حذف شد.', 'success');
modal.hide();
};
}
function prepareAddForm() {
// Reset form for adding new customer
const form = $('#form-add-new-record')[0];
form.reset();
$('#customer-id').val('');
$('#exampleModalLabel').text('افزودن کاربر جدید');
$('.data-submit').text('ذخیره');
// Clear validation errors
$('.is-invalid').removeClass('is-invalid');
$('.invalid-feedback').remove();
// Open modal
$('#add-new-record').offcanvas('show');
}
</script>
{% endblock %}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,137 @@
{% extends '_base.html' %}
{% load static %}
{% block layout %}
layout-wide customizer-hide
{% endblock layout %}
{% block title %}
ورود
{% endblock title %}
{% block style %}
<link rel="stylesheet" href="{% static 'assets/vendor/css/pages/page-auth.css' %}">
{% endblock style %}
{% block content %}
<div class="container-xxl">
<div class="authentication-wrapper authentication-basic container-p-y">
<div class="authentication-inner">
<!-- Register -->
<div class="card">
<div class="card-body">
<!-- Logo -->
<div class="app-brand justify-content-center">
<a href="index.html" class="app-brand-link gap-2">
<span class="app-brand-logo demo">
<svg width="25" viewBox="0 0 25 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M13.7918663,0.358365126 L3.39788168,7.44174259 C0.566865006,9.69408886 -0.379795268,12.4788597 0.557900856,15.7960551 C0.68998853,16.2305145 1.09562888,17.7872135 3.12357076,19.2293357 C3.8146334,19.7207684 5.32369333,20.3834223 7.65075054,21.2172976 L7.59773219,21.2525164 L2.63468769,24.5493413 C0.445452254,26.3002124 0.0884951797,28.5083815 1.56381646,31.1738486 C2.83770406,32.8170431 5.20850219,33.2640127 7.09180128,32.5391577 C8.347334,32.0559211 11.4559176,30.0011079 16.4175519,26.3747182 C18.0338572,24.4997857 18.6973423,22.4544883 18.4080071,20.2388261 C17.963753,17.5346866 16.1776345,15.5799961 13.0496516,14.3747546 L10.9194936,13.4715819 L18.6192054,7.984237 L13.7918663,0.358365126 Z" id="path-1"></path>
<path d="M5.47320593,6.00457225 C4.05321814,8.216144 4.36334763,10.0722806 6.40359441,11.5729822 C8.61520715,12.571656 10.0999176,13.2171421 10.8577257,13.5094407 L15.5088241,14.433041 L18.6192054,7.984237 C15.5364148,3.11535317 13.9273018,0.573395879 13.7918663,0.358365126 C13.5790555,0.511491653 10.8061687,2.3935607 5.47320593,6.00457225 Z" id="path-3"></path>
<path d="M7.50063644,21.2294429 L12.3234468,23.3159332 C14.1688022,24.7579751 14.397098,26.4880487 13.008334,28.506154 C11.6195701,30.5242593 10.3099883,31.790241 9.07958868,32.3040991 C5.78142938,33.4346997 4.13234973,34 4.13234973,34 C4.13234973,34 2.75489982,33.0538207 2.37032616e-14,31.1614621 C-0.55822714,27.8186216 -0.55822714,26.0572515 -4.05231404e-15,25.8773518 C0.83734071,25.6075023 2.77988457,22.8248993 3.3049379,22.52991 C3.65497346,22.3332504 5.05353963,21.8997614 7.50063644,21.2294429 Z" id="path-4"></path>
<path d="M20.6,7.13333333 L25.6,13.8 C26.2627417,14.6836556 26.0836556,15.9372583 25.2,16.6 C24.8538077,16.8596443 24.4327404,17 24,17 L14,17 C12.8954305,17 12,16.1045695 12,15 C12,14.5672596 12.1403557,14.1461923 12.4,13.8 L17.4,7.13333333 C18.0627417,6.24967773 19.3163444,6.07059163 20.2,6.73333333 C20.3516113,6.84704183 20.4862915,6.981722 20.6,7.13333333 Z" id="path-5"></path>
</defs>
<g id="g-app-brand" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Brand-Logo" transform="translate(-27.000000, -15.000000)">
<g id="Icon" transform="translate(27.000000, 15.000000)">
<g id="Mask" transform="translate(0.000000, 8.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use fill="#696cff" xlink:href="#path-1"></use>
<g id="Path-3" mask="url(#mask-2)">
<use fill="#696cff" xlink:href="#path-3"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-3"></use>
</g>
<g id="Path-4" mask="url(#mask-2)">
<use fill="#696cff" xlink:href="#path-4"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-4"></use>
</g>
</g>
<g id="Triangle" transform="translate(19.000000, 11.000000) rotate(-300.000000) translate(-19.000000, -11.000000) ">
<use fill="#696cff" xlink:href="#path-5"></use>
<use fill-opacity="0.2" fill="#FFFFFF" xlink:href="#path-5"></use>
</g>
</g>
</g>
</g>
</svg>
</span>
<span class="app-brand-text demo text-body fw-bold">سامانه شفافیت</span>
</a>
</div>
<!-- /Logo -->
<h4 class="mb-2">Welcome to Sneat! 👋</h4>
<p class="mb-4">Please sign-in to your account and start the adventure</p>
<form id="formAuthentication" class="mb-3 fv-plugins-bootstrap5 fv-plugins-framework" method="post" novalidate="novalidate">
{% csrf_token %}
<div class="mb-3 fv-plugins-icon-container">
<label for="email" class="form-label">Email or Username</label>
<input type="text" class="form-control" id="email" name="username" placeholder="Enter your email or username" autofocus="">
<div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div></div>
<div class="mb-3 form-password-toggle fv-plugins-icon-container">
<div class="d-flex justify-content-between">
<label class="form-label" for="password">Password</label>
<a href="auth-forgot-password-basic.html">
<small>Forgot Password?</small>
</a>
</div>
<div class="input-group input-group-merge has-validation">
<input type="password" id="password" class="form-control" name="password" placeholder="············" aria-describedby="password">
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div><div class="fv-plugins-message-container fv-plugins-message-container--enabled invalid-feedback"></div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="remember-me">
<label class="form-check-label" for="remember-me">
Remember Me
</label>
</div>
</div>
<div class="mb-3">
<button class="btn btn-primary d-grid w-100" type="submit">Sign in</button>
</div>
<input type="hidden"></form>
<p class="text-center">
<span>New on our platform?</span>
<a href="auth-register-basic.html">
<span>Create an account</span>
</a>
</p>
<div class="divider my-4">
<div class="divider-text">or</div>
</div>
<div class="d-flex justify-content-center">
<a href="javascript:;" class="btn btn-icon btn-label-facebook me-3">
<i class="tf-icons bx bxl-facebook"></i>
</a>
<a href="javascript:;" class="btn btn-icon btn-label-google-plus me-3">
<i class="tf-icons bx bxl-google-plus"></i>
</a>
<a href="javascript:;" class="btn btn-icon btn-label-twitter">
<i class="tf-icons bx bxl-twitter"></i>
</a>
</div>
</div>
</div>
<!-- /Register -->
</div>
</div>
</div>
{% endblock content %}
{% block scripts %}
<script src="{% static 'assets/js/pages-auth.js' %}"></script>
{% endblock scripts %}

View file

@ -0,0 +1,60 @@
from django import template
from common.consts import UserRoles
register = template.Library()
@register.filter
def has_item(list, item):
return item in list
def _has_profile(user):
return hasattr(user, "profile") and user.profile
@register.filter
def has_role(user, role_slugs):
return _has_profile(user) and len(user.profile.roles.filter(slug__in=role_slugs.split(","))) > 0
@register.filter
def is_admin(user):
return _has_profile(user) and user.profile.has_role(UserRoles.ADMIN)
@register.filter
def is_manager(user):
return _has_profile(user) and user.profile.has_role(UserRoles.MANAGER)
@register.filter
def is_accountant(user):
return _has_profile(user) and user.profile.has_role(UserRoles.ACCOUNTANT)
@register.filter
def is_broker(user):
return _has_profile(user) and user.profile.has_role(UserRoles.BROKER)
@register.filter
def is_installer(user):
return _has_profile(user) and user.profile.has_role(UserRoles.INSTALLER)
@register.filter
def is_regional_water_authority(user):
return _has_profile(user) and user.profile.has_role(UserRoles.REGIONAL_WATER_AUTHORITY)
@register.filter
def is_water_resource_manager(user):
return _has_profile(user) and user.profile.has_role(UserRoles.WATER_RESOURCE_MANAGER)
@register.filter
def is_headquarter(user):
return _has_profile(user) and user.profile.has_role(UserRoles.HEADQUARTER)
@register.filter
def is_customer(user):
return _has_profile(user) and user.profile.has_role(UserRoles.CUSTOMER)

3
accounts/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

13
accounts/urls.py Normal file
View file

@ -0,0 +1,13 @@
from django.urls import path
from accounts.views import login_view, dashboard, customer_list, add_customer_ajax, edit_customer_ajax, get_customer_data
app_name = "accounts"
urlpatterns = [
path('login/', login_view, name='login'),
path('dashboard/', dashboard, name='dashboard'),
path('customers/', customer_list, name='customer_list'),
path('customers/add/', add_customer_ajax, name='add_customer_ajax'),
path('customers/<int:customer_id>/data/', get_customer_data, name='get_customer_data'),
path('customers/<int:customer_id>/edit/', edit_customer_ajax, name='edit_customer_ajax'),
]

163
accounts/views.py Normal file
View file

@ -0,0 +1,163 @@
from django.contrib import messages
from django.contrib.auth import login, authenticate
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.views.decorators.http import require_POST, require_GET
from django.views.decorators.csrf import csrf_exempt
from django import forms
from accounts.models import Profile
from accounts.forms import CustomerForm
from common.consts import UserRoles
# Create your views here.
def login_view(request):
"""
renders login page and authenticating user POST requests
to log user in
"""
if request.method == "POST":
username = request.POST.get("username")
password = request.POST.get("password")
user = authenticate(request, username=username, password=password)
# if user is not None:
# login(request, user)
# if user.profile.has_none_of([UserRoles.MANAGER]):
# return redirect("dashboard:dashboard")
# else:
# return redirect("dashboard:admin_dashboard")
# else:
# messages.error(request, "کاربری با این مشخصات یافت نشد!")
# return redirect("accounts:login")
return render(request, "accounts/login.html")
def dashboard(request):
return render(request, "accounts/dashboard.html")
def customer_list(request):
# Get all profiles that have customer role
customers = Profile.objects.filter(roles__slug=UserRoles.CUSTOMER.value, is_deleted=False).select_related('user')
form = CustomerForm()
return render(request, "accounts/customer_list.html", {
"customers": customers,
"form": form
})
@require_POST
def add_customer_ajax(request):
"""AJAX endpoint for adding customers"""
form = CustomerForm(request.POST, request.FILES)
form.request = request # Pass request to form
if form.is_valid():
try:
customer = form.save()
return JsonResponse({
'success': True,
'message': 'مشترک با موفقیت اضافه شد!',
'customer': {
'id': customer.id,
'name': customer.user.get_full_name(),
'username': customer.user.username,
'phone': customer.phone_number_1 or 'ثبت نشده',
'national_code': customer.national_code or 'ثبت نشده',
'status': 'تکمیل شده' if customer.is_completed else 'ناقص'
}
})
except forms.ValidationError as e:
return JsonResponse({
'success': False,
'message': str(e)
})
except Exception as e:
return JsonResponse({
'success': False,
'message': f'خطا در ذخیره مشترک: {str(e)}'
})
else:
return JsonResponse({
'success': False,
'message': 'خطا در اعتبارسنجی فرم',
'errors': form.errors
})
@require_POST
def edit_customer_ajax(request, customer_id):
customer = get_object_or_404(Profile, id=customer_id)
form = CustomerForm(request.POST, request.FILES, instance=customer)
form.request = request # Pass request to form
if form.is_valid():
try:
customer = form.save()
return JsonResponse({
'success': True,
'message': 'مشترک با موفقیت ویرایش شد!',
'customer': {
'id': customer.id,
'name': customer.user.get_full_name(),
'username': customer.user.username,
'phone': customer.phone_number_1 or 'ثبت نشده',
'national_code': customer.national_code or 'ثبت نشده',
'status': 'تکمیل شده' if customer.is_completed else 'ناقص'
}
})
except forms.ValidationError as e:
return JsonResponse({
'success': False,
'message': str(e)
})
except Exception as e:
return JsonResponse({
'success': False,
'message': f'خطا در ویرایش مشترک: {str(e)}'
})
else:
return JsonResponse({
'success': False,
'message': 'خطا در اعتبارسنجی فرم',
'errors': form.errors
})
@require_GET
def get_customer_data(request, customer_id):
customer = get_object_or_404(Profile, id=customer_id)
# Create form with existing customer data
form = CustomerForm(instance=customer, initial={
'first_name': customer.user.first_name,
'last_name': customer.user.last_name,
})
# Render form fields as HTML
form_html = {
'first_name': str(form['first_name']),
'last_name': str(form['last_name']),
'phone_number_1': str(form['phone_number_1']),
'phone_number_2': str(form['phone_number_2']),
'national_code': str(form['national_code']),
'card_number': str(form['card_number']),
'account_number': str(form['account_number']),
'address': str(form['address']),
}
return JsonResponse({
'success': True,
'customer': {
'id': customer.id,
'first_name': customer.user.first_name,
'last_name': customer.user.last_name,
'phone_number_1': customer.phone_number_1 or '',
'phone_number_2': customer.phone_number_2 or '',
'national_code': customer.national_code or '',
'card_number': customer.card_number or '',
'account_number': customer.account_number or '',
'address': customer.address or ''
},
'form_html': form_html
})

0
common/__init__.py Normal file
View file

3
common/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
common/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CommonConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'common'

13
common/consts.py Normal file
View file

@ -0,0 +1,13 @@
from enum import Enum
class UserRoles(Enum):
ADMIN = "adm" #ادمین
CUSTOMER = "cus" #مشترک
MANAGER = "mng" #مدیر
ACCOUNTANT = "aco" #حسابدار
BROKER = "bro" # کارگزار - پیشخان
INSTALLER = "inst" # نصاب
REGIONAL_WATER_AUTHORITY = "rwa" # کارشناس امور
WATER_RESOURCE_MANAGER = "wrm" # مدیر منابع آب
HEADQUARTER = "hdq" # ستاد آب منطقه‌ای

View file

@ -0,0 +1,9 @@
from datetime import datetime
def current_year(request):
"""
Context processor to add current year to all templates
"""
return {
'current_year': datetime.now().year
}

67
common/decorators.py Normal file
View file

@ -0,0 +1,67 @@
from functools import wraps
from django.http import JsonResponse, HttpResponse
from django.shortcuts import redirect
from extensions.consts import UserRoles
def require_ajax(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'error': 'Only AJAX requests are allowed.'}, status=400)
return view_func(request, *args, **kwargs)
return _wrapped_view
def allowed_roles(allowed_roles: list[UserRoles]):
"""
@param allowed_roles must not be empty
"""
def decorator(views_func):
def wrapper_func(request, *args, **kwargs):
roles = [role.slug for role in request.user.profile.roles.all()]
allowed_role_names = [role.value for role in allowed_roles]
if any(item in roles for item in allowed_role_names):
return views_func(request, *args, **kwargs)
else:
return HttpResponse('you are not allow', status=401)
return wrapper_func
return decorator
def profile_complete_needed(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if not request.user.profile or not request.user.profile.is_completed:
return redirect("accounts:profile")
return view_func(request, *args, **kwargs)
return _wrapped_view
def superuser_required(views_func):
def wrapper_func(request, *args, **kwargs):
user = request.user
if user.is_superuser:
return views_func(request, *args, **kwargs)
else:
return redirect('dashboard:vodDashboard')
return wrapper_func
def staffuser_required(views_func):
def wrapper_func(request, *args, **kwargs):
user = request.user
if user.is_staff:
return views_func(request, *args, **kwargs)
else:
return redirect('dashboard:vodDashboard')
return wrapper_func

View file

128
common/models.py Normal file
View file

@ -0,0 +1,128 @@
from django.contrib import admin
from django.db import models
from django.utils import timezone
from _helpers.utils import jalali_converter, generate_unique_slug
class ObjectsQuerySet(models.QuerySet):
def deleted_objects(self):
return self.filter(is_deleted=True)
def available_objects(self):
return self.filter(is_deleted=False)
def active_objects(self):
return self.filter(is_active=True)
def inactive_objects(self):
return self.filter(is_active=False)
class BaseModel(models.Model):
created = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد")
updated = models.DateTimeField(auto_now=True, verbose_name="تاریخ بروزرسانی")
is_active = models.BooleanField(default=True, verbose_name="فعال")
is_deleted = models.BooleanField(default=False, verbose_name="حذف شده")
deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ حذف")
objects = ObjectsQuerySet.as_manager()
class Meta:
abstract = True
def save(self, *args, **kwargs):
if self.is_deleted:
self.is_active = False
super(BaseModel, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
def hard_delete(self):
super().delete()
def jcreated(self):
return jalali_converter(self.created)
jcreated.short_description = "تاریخ ایجاد"
def jcreated_date(self):
return self.jcreated().split(',')[0]
jcreated_date.short_description = "تاریخ ایجاد"
def jupdated(self):
return jalali_converter(self.updated)
jupdated.short_description = "تاریخ بروزرسانی"
def jupdated_date(self):
return self.jupdated().split(',')[0]
jupdated_date.short_description = "تاریخ بروزرسانی"
class SluggedModel(BaseModel):
slug = models.SlugField(max_length=100, unique=True, verbose_name="اسلاگ")
class Meta:
abstract = True
def save(self, *args, **kwargs):
if not self.slug:
if hasattr(self, 'name'):
self.slug = generate_unique_slug(self.name)
else:
self.slug = generate_unique_slug(str(self.created))
super(BaseModel, self).save(*args, **kwargs)
class SelfParentModel(BaseModel):
parent = models.ForeignKey('self', related_name='children',
on_delete=models.SET_NULL, null=True, blank=True, default=None,
verbose_name='زیرشاخه')
class Meta:
abstract = True
class NameSlugModel(SluggedModel):
name = models.CharField(max_length=100, verbose_name="نام")
class Meta:
abstract = True
def __str__(self):
return self.name
class NameSlugAdminModel(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
class Meta:
abstract = True
class SelfParentNameSlugModel(SelfParentModel, NameSlugModel):
class Meta:
abstract = True
class TagModel(SelfParentModel, NameSlugModel):
class Meta:
abstract = True
ordering = ['parent__id', 'name']
def __str__(self):
return self.name
class TagAdminModel(admin.ModelAdmin):
fields = ['name', 'slug', 'parent', 'is_active']
list_display = ['name', 'slug', 'parent', 'is_active', 'is_deleted', 'jcreated']
list_filter = ['is_active', 'slug', 'parent', 'is_deleted']
search_fields = ['name', 'slug', 'parent__name']
prepopulated_fields = {'slug': ('name',)}

3
common/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
common/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
invoices/__init__.py Normal file
View file

65
invoices/admin.py Normal file
View file

@ -0,0 +1,65 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .models import Item, Quote, QuoteItem, Invoice, InvoiceItem, Payment
@admin.register(Item)
class ItemAdmin(SimpleHistoryAdmin):
list_display = ['name', 'unit_price', 'default_quantity', 'is_default_in_quotes', 'is_active', 'created_by']
list_filter = ['is_default_in_quotes', 'is_active', 'created_by']
search_fields = ['name', 'description']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['deleted_at', 'created', 'updated']
class QuoteItemInline(admin.TabularInline):
model = QuoteItem
extra = 1
fields = ['item', 'quantity', 'unit_price', 'total_price', 'notes']
@admin.register(Quote)
class QuoteAdmin(SimpleHistoryAdmin):
list_display = ['name', 'process_instance', 'customer', 'status_display', 'total_amount', 'final_amount', 'valid_until', 'created_by']
list_filter = ['status', 'created', 'valid_until', 'process_instance__process']
search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount']
inlines = [QuoteItemInline]
ordering = ['-created']
def status_display(self, obj):
return mark_safe(obj.get_status_display_with_color())
status_display.short_description = "وضعیت"
class InvoiceItemInline(admin.TabularInline):
model = InvoiceItem
extra = 1
fields = ['item', 'quantity', 'unit_price', 'total_price', 'notes']
class PaymentInline(admin.TabularInline):
model = Payment
extra = 1
fields = ['amount', 'payment_method', 'reference_number', 'payment_date', 'notes']
readonly_fields = ['created_by', 'created']
@admin.register(Invoice)
class InvoiceAdmin(SimpleHistoryAdmin):
list_display = ['name', 'process_instance', 'customer', 'status_display', 'final_amount', 'paid_amount', 'remaining_amount', 'due_date']
list_filter = ['status', 'created', 'due_date', 'process_instance__process']
search_fields = ['name', 'customer__username', 'customer__first_name', 'customer__last_name', 'notes']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['deleted_at', 'created', 'updated', 'total_amount', 'discount_amount', 'final_amount', 'paid_amount', 'remaining_amount']
inlines = [InvoiceItemInline, PaymentInline]
ordering = ['-created']
def status_display(self, obj):
return mark_safe(obj.get_status_display_with_color())
status_display.short_description = "وضعیت"
@admin.register(Payment)
class PaymentAdmin(SimpleHistoryAdmin):
list_display = ['invoice', 'amount', 'payment_method', 'payment_date', 'created_by']
list_filter = ['payment_method', 'payment_date', 'created_by']
search_fields = ['invoice__name', 'reference_number', 'notes']
readonly_fields = ['created']
ordering = ['-payment_date']

7
invoices/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class InvoicesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'invoices'
verbose_name = 'فاکتورها'

View file

@ -0,0 +1,363 @@
# Generated by Django 5.2.4 on 2025-08-07 09:08
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('processes', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HistoricalItem',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('description', models.TextField(blank=True, verbose_name='توضیحات')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('default_quantity', models.PositiveIntegerField(default=1, verbose_name='تعداد پیش\u200cفرض')),
('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical آیتم',
'verbose_name_plural': 'historical آیتم\u200cها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalQuote',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('accepted', 'تایید شده'), ('rejected', 'رد شده'), ('expired', 'منقضی شده')], default='draft', max_length=20, verbose_name='وضعیت')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('valid_until', models.DateField(verbose_name='معتبر تا')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('customer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')),
],
options={
'verbose_name': 'historical پیش\u200cفاکتور',
'verbose_name_plural': 'historical پیش\u200cفاکتورها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('paid', 'پرداخت شده'), ('partially_paid', 'نیمه پرداخت شده'), ('overdue', 'معوق'), ('cancelled', 'لغو شده')], default='draft', max_length=20, verbose_name='وضعیت')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ پرداخت شده')),
('remaining_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ باقی\u200cمانده')),
('due_date', models.DateField(verbose_name='تاریخ سررسید')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_invoices', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='processes.processinstance', verbose_name='نمونه فرآیند')),
],
options={
'verbose_name': 'فاکتور',
'verbose_name_plural': 'فاکتورها',
'ordering': ['-created'],
},
),
migrations.CreateModel(
name='HistoricalPayment',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='مبلغ پرداخت')),
('payment_method', models.CharField(choices=[('cash', 'نقدی'), ('bank_transfer', 'انتقال بانکی'), ('check', 'چک'), ('card', 'کارت بانکی'), ('other', 'سایر')], default='cash', max_length=20, verbose_name='روش پرداخت')),
('reference_number', models.CharField(blank=True, max_length=100, verbose_name='شماره مرجع')),
('payment_date', models.DateField(verbose_name='تاریخ پرداخت')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ثبت کننده')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('invoice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.invoice', verbose_name='فاکتور')),
],
options={
'verbose_name': 'historical پرداخت',
'verbose_name_plural': 'historical پرداخت\u200cها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('description', models.TextField(blank=True, verbose_name='توضیحات')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('default_quantity', models.PositiveIntegerField(default=1, verbose_name='تعداد پیش\u200cفرض')),
('is_default_in_quotes', models.BooleanField(default=False, help_text='این آیتم به صورت پیش\u200cفرض در همه پیش\u200cفاکتورها قرار می\u200cگیرد', verbose_name='پیش\u200cفرض در پیش\u200cفاکتورها')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
],
options={
'verbose_name': 'آیتم',
'verbose_name_plural': 'آیتم\u200cها',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='InvoiceItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.invoice', verbose_name='فاکتور')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item', verbose_name='آیتم')),
],
options={
'verbose_name': 'آیتم فاکتور',
'verbose_name_plural': 'آیتم\u200cهای فاکتور',
'ordering': ['invoice', 'item__name'],
},
),
migrations.CreateModel(
name='HistoricalInvoiceItem',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('invoice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.invoice', verbose_name='فاکتور')),
('item', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.item', verbose_name='آیتم')),
],
options={
'verbose_name': 'historical آیتم فاکتور',
'verbose_name_plural': 'historical آیتم\u200cهای فاکتور',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='مبلغ پرداخت')),
('payment_method', models.CharField(choices=[('cash', 'نقدی'), ('bank_transfer', 'انتقال بانکی'), ('check', 'چک'), ('card', 'کارت بانکی'), ('other', 'سایر')], default='cash', max_length=20, verbose_name='روش پرداخت')),
('reference_number', models.CharField(blank=True, max_length=100, verbose_name='شماره مرجع')),
('payment_date', models.DateField(verbose_name='تاریخ پرداخت')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ثبت کننده')),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='invoices.invoice', verbose_name='فاکتور')),
],
options={
'verbose_name': 'پرداخت',
'verbose_name_plural': 'پرداخت\u200cها',
'ordering': ['-payment_date'],
},
),
migrations.CreateModel(
name='Quote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('accepted', 'تایید شده'), ('rejected', 'رد شده'), ('expired', 'منقضی شده')], default='draft', max_length=20, verbose_name='وضعیت')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('valid_until', models.DateField(verbose_name='معتبر تا')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_quotes', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='processes.processinstance', verbose_name='نمونه فرآیند')),
],
options={
'verbose_name': 'پیش\u200cفاکتور',
'verbose_name_plural': 'پیش\u200cفاکتورها',
'ordering': ['-created'],
},
),
migrations.AddField(
model_name='invoice',
name='quote',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='invoices.quote', verbose_name='پیش\u200cفاکتور مربوطه'),
),
migrations.CreateModel(
name='HistoricalQuoteItem',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('item', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.item', verbose_name='آیتم')),
('quote', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.quote', verbose_name='پیش\u200cفاکتور')),
],
options={
'verbose_name': 'historical آیتم پیش\u200cفاکتور',
'verbose_name_plural': 'historical آیتم\u200cهای پیش\u200cفاکتور',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalInvoice',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('draft', 'پیش\u200cنویس'), ('sent', 'ارسال شده'), ('paid', 'پرداخت شده'), ('partially_paid', 'نیمه پرداخت شده'), ('overdue', 'معوق'), ('cancelled', 'لغو شده')], default='draft', max_length=20, verbose_name='وضعیت')),
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ کل')),
('discount_percent', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='درصد تخفیف')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ تخفیف')),
('final_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ نهایی')),
('paid_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ پرداخت شده')),
('remaining_amount', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='مبلغ باقی\u200cمانده')),
('due_date', models.DateField(verbose_name='تاریخ سررسید')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('customer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='مشترک')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')),
('quote', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='invoices.quote', verbose_name='پیش\u200cفاکتور مربوطه')),
],
options={
'verbose_name': 'historical فاکتور',
'verbose_name_plural': 'historical فاکتورها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='QuoteItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('quantity', models.PositiveIntegerField(verbose_name='تعداد')),
('unit_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت واحد')),
('total_price', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='قیمت کل')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='invoices.item', verbose_name='آیتم')),
('quote', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoices.quote', verbose_name='پیش\u200cفاکتور')),
],
options={
'verbose_name': 'آیتم پیش\u200cفاکتور',
'verbose_name_plural': 'آیتم\u200cهای پیش\u200cفاکتور',
'ordering': ['quote', 'item__name'],
},
),
]

View file

331
invoices/models.py Normal file
View file

@ -0,0 +1,331 @@
from django.db import models
from django.contrib.auth import get_user_model
from common.models import NameSlugModel, BaseModel
from simple_history.models import HistoricalRecords
from django.core.exceptions import ValidationError
from decimal import Decimal
User = get_user_model()
class Item(NameSlugModel):
"""مدل آیتم‌های پیش‌فرض"""
description = models.TextField(verbose_name="توضیحات", blank=True)
unit_price = models.DecimalField(
max_digits=15,
decimal_places=2,
verbose_name="قیمت واحد"
)
default_quantity = models.PositiveIntegerField(
default=1,
verbose_name="تعداد پیش‌فرض"
)
is_default_in_quotes = models.BooleanField(
default=False,
verbose_name="پیش‌فرض در پیش‌فاکتورها",
help_text="این آیتم به صورت پیش‌فرض در همه پیش‌فاکتورها قرار می‌گیرد"
)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ایجاد کننده")
history = HistoricalRecords()
class Meta:
verbose_name = "آیتم"
verbose_name_plural = "آیتم‌ها"
ordering = ['name']
def __str__(self):
return f"{self.name} - {self.unit_price} تومان"
class Quote(NameSlugModel):
"""مدل پیش‌فاکتور"""
process_instance = models.ForeignKey(
'processes.ProcessInstance',
on_delete=models.CASCADE,
verbose_name="نمونه فرآیند"
)
customer = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="مشترک")
status = models.CharField(
max_length=20,
choices=[
('draft', 'پیش‌نویس'),
('sent', 'ارسال شده'),
('accepted', 'تایید شده'),
('rejected', 'رد شده'),
('expired', 'منقضی شده'),
],
default='draft',
verbose_name="وضعیت"
)
total_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0,
verbose_name="مبلغ کل"
)
discount_percent = models.DecimalField(
max_digits=5,
decimal_places=2,
default=0,
verbose_name="درصد تخفیف"
)
discount_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0,
verbose_name="مبلغ تخفیف"
)
final_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0,
verbose_name="مبلغ نهایی"
)
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)
valid_until = models.DateField(verbose_name="معتبر تا")
created_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name="ایجاد کننده",
related_name='created_quotes'
)
history = HistoricalRecords()
class Meta:
verbose_name = "پیش‌فاکتور"
verbose_name_plural = "پیش‌فاکتورها"
ordering = ['-created']
def __str__(self):
return f"پیش‌فاکتور {self.name} - {self.customer.get_full_name()}"
def calculate_totals(self):
"""محاسبه مبالغ کل"""
total = sum(item.total_price for item in self.items.all())
self.total_amount = total
# محاسبه تخفیف
if self.discount_percent > 0:
self.discount_amount = (total * self.discount_percent) / 100
else:
self.discount_amount = 0
self.final_amount = self.total_amount - self.discount_amount
self.save()
def get_status_display_with_color(self):
"""نمایش وضعیت با رنگ"""
status_colors = {
'draft': 'secondary',
'sent': 'primary',
'accepted': 'success',
'rejected': 'danger',
'expired': 'warning',
}
color = status_colors.get(self.status, 'secondary')
return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
class QuoteItem(BaseModel):
"""مدل آیتم‌های پیش‌فاکتور"""
quote = models.ForeignKey(Quote, on_delete=models.CASCADE, related_name='items', verbose_name="پیش‌فاکتور")
item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name="آیتم")
quantity = models.PositiveIntegerField(verbose_name="تعداد")
unit_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت واحد")
total_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت کل")
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)
history = HistoricalRecords()
class Meta:
verbose_name = "آیتم پیش‌فاکتور"
verbose_name_plural = "آیتم‌های پیش‌فاکتور"
ordering = ['quote', 'item__name']
def __str__(self):
return f"{self.item.name} - {self.quantity} عدد"
def save(self, *args, **kwargs):
"""محاسبه قیمت کل"""
self.total_price = self.quantity * self.unit_price
super().save(*args, **kwargs)
# بروزرسانی مبالغ پیش‌فاکتور
self.quote.calculate_totals()
class Invoice(NameSlugModel):
"""مدل فاکتور نهایی"""
quote = models.ForeignKey(
Quote,
on_delete=models.CASCADE,
verbose_name="پیش‌فاکتور مربوطه",
null=True,
blank=True
)
process_instance = models.ForeignKey(
'processes.ProcessInstance',
on_delete=models.CASCADE,
verbose_name="نمونه فرآیند"
)
customer = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="مشترک")
status = models.CharField(
max_length=20,
choices=[
('draft', 'پیش‌نویس'),
('sent', 'ارسال شده'),
('paid', 'پرداخت شده'),
('partially_paid', 'نیمه پرداخت شده'),
('overdue', 'معوق'),
('cancelled', 'لغو شده'),
],
default='draft',
verbose_name="وضعیت"
)
total_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0,
verbose_name="مبلغ کل"
)
discount_percent = models.DecimalField(
max_digits=5,
decimal_places=2,
default=0,
verbose_name="درصد تخفیف"
)
discount_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0,
verbose_name="مبلغ تخفیف"
)
final_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0,
verbose_name="مبلغ نهایی"
)
paid_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0,
verbose_name="مبلغ پرداخت شده"
)
remaining_amount = models.DecimalField(
max_digits=15,
decimal_places=2,
default=0,
verbose_name="مبلغ باقی‌مانده"
)
due_date = models.DateField(verbose_name="تاریخ سررسید")
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)
created_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name="ایجاد کننده",
related_name='created_invoices'
)
history = HistoricalRecords()
class Meta:
verbose_name = "فاکتور"
verbose_name_plural = "فاکتورها"
ordering = ['-created']
def __str__(self):
return f"فاکتور {self.name} - {self.customer.get_full_name()}"
def calculate_totals(self):
"""محاسبه مبالغ کل"""
total = sum(item.total_price for item in self.items.all())
self.total_amount = total
# محاسبه تخفیف
if self.discount_percent > 0:
self.discount_amount = (total * self.discount_percent) / 100
else:
self.discount_amount = 0
self.final_amount = self.total_amount - self.discount_amount
self.remaining_amount = self.final_amount - self.paid_amount
# بروزرسانی وضعیت
if self.remaining_amount <= 0:
self.status = 'paid'
elif self.paid_amount > 0:
self.status = 'partially_paid'
else:
self.status = 'sent'
self.save()
def get_status_display_with_color(self):
"""نمایش وضعیت با رنگ"""
status_colors = {
'draft': 'secondary',
'sent': 'primary',
'paid': 'success',
'partially_paid': 'info',
'overdue': 'danger',
'cancelled': 'warning',
}
color = status_colors.get(self.status, 'secondary')
return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
class InvoiceItem(BaseModel):
"""مدل آیتم‌های فاکتور"""
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='items', verbose_name="فاکتور")
item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name="آیتم")
quantity = models.PositiveIntegerField(verbose_name="تعداد")
unit_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت واحد")
total_price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="قیمت کل")
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)
history = HistoricalRecords()
class Meta:
verbose_name = "آیتم فاکتور"
verbose_name_plural = "آیتم‌های فاکتور"
ordering = ['invoice', 'item__name']
def __str__(self):
return f"{self.item.name} - {self.quantity} عدد"
def save(self, *args, **kwargs):
"""محاسبه قیمت کل"""
self.total_price = self.quantity * self.unit_price
super().save(*args, **kwargs)
# بروزرسانی مبالغ فاکتور
self.invoice.calculate_totals()
class Payment(BaseModel):
"""مدل پرداخت‌ها"""
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='payments', verbose_name="فاکتور")
amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name="مبلغ پرداخت")
payment_method = models.CharField(
max_length=20,
choices=[
('cash', 'نقدی'),
('bank_transfer', 'انتقال بانکی'),
('check', 'چک'),
('card', 'کارت بانکی'),
('other', 'سایر'),
],
default='cash',
verbose_name="روش پرداخت"
)
reference_number = models.CharField(max_length=100, verbose_name="شماره مرجع", blank=True)
payment_date = models.DateField(verbose_name="تاریخ پرداخت")
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ثبت کننده")
history = HistoricalRecords()
class Meta:
verbose_name = "پرداخت"
verbose_name_plural = "پرداخت‌ها"
ordering = ['-payment_date']
def __str__(self):
return f"پرداخت {self.amount} تومان - {self.invoice.name}"
def save(self, *args, **kwargs):
"""بروزرسانی مبالغ فاکتور"""
super().save(*args, **kwargs)
# بروزرسانی مبلغ پرداخت شده فاکتور
total_paid = sum(payment.amount for payment in self.invoice.payments.all())
self.invoice.paid_amount = total_paid
self.invoice.calculate_totals()

3
invoices/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
invoices/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
locations/__init__.py Normal file
View file

35
locations/admin.py Normal file
View file

@ -0,0 +1,35 @@
from django.contrib import admin
from .models import City, County, Affairs, Broker
# Register your models here.
@admin.register(City)
class CityAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
search_fields = ['name']
readonly_fields = ['deleted_at']
prepopulated_fields = {'slug': ('name',)}
@admin.register(County)
class CountyAdmin(admin.ModelAdmin):
list_display = ['name', 'city', 'slug']
list_filter = ['city']
search_fields = ['name', 'city__name']
readonly_fields = ['deleted_at']
prepopulated_fields = {'slug': ('name',)}
@admin.register(Affairs)
class AffairsAdmin(admin.ModelAdmin):
list_display = ['name', 'county', 'slug']
list_filter = ['county__city', 'county']
search_fields = ['name', 'county__name', 'county__city__name']
readonly_fields = ['deleted_at']
prepopulated_fields = {'slug': ('name',)}
@admin.register(Broker)
class BrokerAdmin(admin.ModelAdmin):
list_display = ['name', 'affairs', 'slug']
list_filter = ['affairs__county__city', 'affairs__county', 'affairs']
search_fields = ['name', 'affairs__name', 'affairs__county__name']
readonly_fields = ['deleted_at']
prepopulated_fields = {'slug': ('name',)}

7
locations/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class LocationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'locations'
verbose_name = "مکان‌ها"

View file

@ -0,0 +1,90 @@
# Generated by Django 5.2.4 on 2025-08-07 09:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Affairs',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
],
options={
'verbose_name': 'امور',
'verbose_name_plural': 'امورها',
},
),
migrations.CreateModel(
name='City',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
],
options={
'verbose_name': 'شهر',
'verbose_name_plural': 'شهرها',
},
),
migrations.CreateModel(
name='Broker',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('affairs', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='locations.affairs', verbose_name='امور')),
],
options={
'verbose_name': 'کارگزار',
'verbose_name_plural': 'کارگزارها',
},
),
migrations.CreateModel(
name='County',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='locations.city', verbose_name='شهرستان')),
],
options={
'verbose_name': 'شهرستان',
'verbose_name_plural': 'شهرستان\u200cها',
},
),
migrations.AddField(
model_name='affairs',
name='county',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='locations.county', verbose_name='شهرستان'),
),
]

View file

41
locations/models.py Normal file
View file

@ -0,0 +1,41 @@
from django.db import models
from common.models import NameSlugModel
# Create your models here.
class City(NameSlugModel):
class Meta:
verbose_name = "شهر"
verbose_name_plural = "شهرها"
def __str__(self):
return self.name
class County(NameSlugModel):
city = models.ForeignKey(City, on_delete=models.CASCADE, verbose_name="شهرستان")
class Meta:
verbose_name = "شهرستان"
verbose_name_plural = "شهرستان‌ها"
def __str__(self):
return self.name
class Affairs(NameSlugModel):
county = models.ForeignKey(County, on_delete=models.CASCADE, verbose_name="شهرستان")
class Meta:
verbose_name = "امور"
verbose_name_plural = "امورها"
def __str__(self):
return self.name
class Broker(NameSlugModel):
affairs = models.ForeignKey(Affairs, on_delete=models.CASCADE, verbose_name="امور")
class Meta:
verbose_name = "کارگزار"
verbose_name_plural = "کارگزارها"
def __str__(self):
return self.name

3
locations/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
locations/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

22
manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '_base.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
processes/__init__.py Normal file
View file

107
processes/admin.py Normal file
View file

@ -0,0 +1,107 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .models import Process, ProcessStep, ProcessInstance, StepInstance, StepDependency, StepRejection, StepRevision
@admin.register(Process)
class ProcessAdmin(SimpleHistoryAdmin):
list_display = ['name', 'is_active', 'created_by', 'created', 'steps_count']
list_filter = ['is_active', 'created']
search_fields = ['name', 'description', 'created_by__username']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['deleted_at', 'created', 'updated']
def steps_count(self, obj):
return obj.steps.count()
steps_count.short_description = "تعداد مراحل"
@admin.register(ProcessStep)
class ProcessStepAdmin(SimpleHistoryAdmin):
list_display = ['name', 'process', 'order', 'is_required', 'dependencies_display', 'blocks_previous', 'can_go_back']
list_filter = ['process', 'is_required', 'blocks_previous', 'can_go_back']
search_fields = ['name', 'process__name', 'description']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['deleted_at']
ordering = ['process', 'order']
def dependencies_display(self, obj):
dependencies = obj.get_dependencies()
if dependencies:
dependency_names = ProcessStep.objects.filter(id__in=dependencies).values_list('name', flat=True)
return ", ".join(dependency_names)
return "بدون وابستگی"
dependencies_display.short_description = "وابسته به"
@admin.register(StepDependency)
class StepDependencyAdmin(admin.ModelAdmin):
list_display = ['dependent_step', 'dependency_step', 'process_display', 'created_at']
list_filter = ['dependent_step__process', 'created_at']
search_fields = ['dependent_step__name', 'dependency_step__name']
ordering = ['dependent_step__process', 'dependent_step__order']
def process_display(self, obj):
return obj.dependent_step.process.name
process_display.short_description = "فرآیند"
@admin.register(ProcessInstance)
class ProcessInstanceAdmin(SimpleHistoryAdmin):
list_display = ['name', 'process', 'requester', 'current_step', 'status', 'started_at', 'progress_display']
list_filter = ['process', 'status', 'started_at']
search_fields = ['name', 'process__name', 'requester__username', 'requester__first_name']
readonly_fields = ['deleted_at', 'started_at', 'completed_at']
ordering = ['-started_at']
def progress_display(self, obj):
total_steps = obj.process.steps.count()
completed_steps = obj.step_instances.filter(status='completed').count()
percentage = (completed_steps / total_steps * 100) if total_steps > 0 else 0
percentage_int = int(percentage)
return format_html(
'<div class="progress" style="width: 100px;"><div class="progress-bar" style="width: {}%">{}/{} ({}%)</div></div>',
percentage_int, completed_steps, total_steps, percentage_int
)
progress_display.short_description = "پیشرفت"
@admin.register(StepInstance)
class StepInstanceAdmin(SimpleHistoryAdmin):
list_display = ['process_instance', 'step', 'assigned_to', 'status_display', 'rejection_count', 'started_at', 'completed_at']
list_filter = ['status', 'step__process', 'started_at']
search_fields = ['process_instance__name', 'step__name', 'assigned_to__username']
readonly_fields = ['started_at', 'completed_at']
ordering = ['process_instance', 'step__order']
def status_display(self, obj):
return mark_safe(obj.get_status_display_with_color())
status_display.short_description = "وضعیت"
def rejection_count(self, obj):
count = obj.get_rejection_count()
if count > 0:
return format_html('<span class="badge bg-danger">{}</span>', count)
return format_html('<span class="badge bg-success">0</span>')
rejection_count.short_description = "تعداد رد شدن‌ها"
@admin.register(StepRejection)
class StepRejectionAdmin(SimpleHistoryAdmin):
list_display = ['step_instance', 'rejected_by', 'reason_short', 'created_at']
list_filter = ['rejected_by', 'created_at', 'step_instance__step__process']
search_fields = ['step_instance__step__name', 'rejected_by__username', 'reason']
readonly_fields = ['created_at']
ordering = ['-created_at']
def reason_short(self, obj):
return obj.reason[:50] + "..." if len(obj.reason) > 50 else obj.reason
reason_short.short_description = "دلیل رد شدن"
@admin.register(StepRevision)
class StepRevisionAdmin(SimpleHistoryAdmin):
list_display = ['step_instance', 'rejection', 'revised_by', 'changes_short', 'created_at']
list_filter = ['revised_by', 'created_at', 'step_instance__step__process']
search_fields = ['step_instance__step__name', 'revised_by__username', 'changes_description']
readonly_fields = ['created_at']
ordering = ['-created_at']
def changes_short(self, obj):
return obj.changes_description[:50] + "..." if len(obj.changes_description) > 50 else obj.changes_description
changes_short.short_description = "تغییرات"

7
processes/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class ProcessesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'processes'
verbose_name = 'فرآیندها'

19
processes/forms.py Normal file
View file

@ -0,0 +1,19 @@
from django import forms
from .models import ProcessInstance, StepInstance
class ProcessInstanceForm(forms.ModelForm):
class Meta:
model = ProcessInstance
fields = ['name']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'})
}
class StepInstanceForm(forms.ModelForm):
class Meta:
model = StepInstance
fields = ['status', 'notes']
widgets = {
'status': forms.Select(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3})
}

View file

@ -0,0 +1,314 @@
# Generated by Django 5.2.4 on 2025-08-07 09:08
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HistoricalProcess',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('description', models.TextField(blank=True, verbose_name='توضیحات')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical فرآیند',
'verbose_name_plural': 'historical فرآیندها',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Process',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('description', models.TextField(blank=True, verbose_name='توضیحات')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='ایجاد کننده')),
],
options={
'verbose_name': 'فرآیند',
'verbose_name_plural': 'فرآیندها',
'ordering': ['-created'],
},
),
migrations.CreateModel(
name='HistoricalProcessStep',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('order', models.PositiveIntegerField(verbose_name='ترتیب')),
('description', models.TextField(blank=True, verbose_name='توضیحات')),
('is_required', models.BooleanField(default=True, verbose_name='اجباری')),
('estimated_duration', models.PositiveIntegerField(blank=True, null=True, verbose_name='مدت زمان تخمینی (روز)')),
('blocks_previous', models.BooleanField(default=False, help_text='اگر فعال باشد، پس از تکمیل این مرحله، مراحل قبلی غیرقابل ویرایش می\u200cشوند', verbose_name='مسدود کننده مراحل قبلی')),
('can_go_back', models.BooleanField(default=True, help_text='آیا می\u200cتوان به مراحل قبلی بازگشت', verbose_name='قابل بازگشت')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('process', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.process', verbose_name='فرآیند')),
],
options={
'verbose_name': 'historical مرحله فرآیند',
'verbose_name_plural': 'historical مراحل فرآیند',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='ProcessStep',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('order', models.PositiveIntegerField(verbose_name='ترتیب')),
('description', models.TextField(blank=True, verbose_name='توضیحات')),
('is_required', models.BooleanField(default=True, verbose_name='اجباری')),
('estimated_duration', models.PositiveIntegerField(blank=True, null=True, verbose_name='مدت زمان تخمینی (روز)')),
('blocks_previous', models.BooleanField(default=False, help_text='اگر فعال باشد، پس از تکمیل این مرحله، مراحل قبلی غیرقابل ویرایش می\u200cشوند', verbose_name='مسدود کننده مراحل قبلی')),
('can_go_back', models.BooleanField(default=True, help_text='آیا می\u200cتوان به مراحل قبلی بازگشت', verbose_name='قابل بازگشت')),
('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='processes.process', verbose_name='فرآیند')),
],
options={
'verbose_name': 'مرحله فرآیند',
'verbose_name_plural': 'مراحل فرآیند',
'ordering': ['process', 'order'],
'unique_together': {('process', 'order')},
},
),
migrations.CreateModel(
name='ProcessInstance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
('started_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ شروع')),
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='processes.process', verbose_name='فرآیند')),
('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
('current_step', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='processes.processstep', verbose_name='مرحله فعلی')),
],
options={
'verbose_name': 'نمونه فرآیند',
'verbose_name_plural': 'نمونه\u200cهای فرآیند',
'ordering': ['-started_at'],
},
),
migrations.CreateModel(
name='HistoricalStepInstance',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('skipped', 'رد شده'), ('blocked', 'مسدود شده'), ('rejected', 'رد شده و نیاز به اصلاح')], default='pending', max_length=20, verbose_name='وضعیت')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('started_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ شروع')),
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('assigned_to', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='واگذار شده به')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('process_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processinstance', verbose_name='نمونه فرآیند')),
('step', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processstep', verbose_name='مرحله')),
],
options={
'verbose_name': 'historical نمونه مرحله',
'verbose_name_plural': 'historical نمونه\u200cهای مرحله',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalProcessInstance',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ ایجاد')),
('updated', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ بروزرسانی')),
('is_active', models.BooleanField(default=True, verbose_name='فعال')),
('is_deleted', models.BooleanField(default=False, verbose_name='حذف شده')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ حذف')),
('slug', models.SlugField(max_length=100, verbose_name='اسلاگ')),
('name', models.CharField(max_length=100, verbose_name='نام')),
('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('cancelled', 'لغو شده'), ('rejected', 'رد شده')], default='pending', max_length=20, verbose_name='وضعیت')),
('started_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ شروع')),
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('requester', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='درخواست کننده')),
('process', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.process', verbose_name='فرآیند')),
('current_step', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.processstep', verbose_name='مرحله فعلی')),
],
options={
'verbose_name': 'historical نمونه فرآیند',
'verbose_name_plural': 'historical نمونه\u200cهای فرآیند',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='StepInstance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('pending', 'در انتظار'), ('in_progress', 'در حال انجام'), ('completed', 'تکمیل شده'), ('skipped', 'رد شده'), ('blocked', 'مسدود شده'), ('rejected', 'رد شده و نیاز به اصلاح')], default='pending', max_length=20, verbose_name='وضعیت')),
('notes', models.TextField(blank=True, verbose_name='یادداشت\u200cها')),
('started_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ شروع')),
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='تاریخ تکمیل')),
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='واگذار شده به')),
('process_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_instances', to='processes.processinstance', verbose_name='نمونه فرآیند')),
('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='processes.processstep', verbose_name='مرحله')),
],
options={
'verbose_name': 'نمونه مرحله',
'verbose_name_plural': 'نمونه\u200cهای مرحله',
'ordering': ['process_instance', 'step__order'],
},
),
migrations.CreateModel(
name='HistoricalStepRejection',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('reason', models.TextField(help_text='توضیح کامل دلیل رد شدن', verbose_name='دلیل رد شدن')),
('instructions', models.TextField(blank=True, help_text='دستورالعمل\u200cهایی برای اصلاح مرحله', verbose_name='دستورالعمل\u200cهای اصلاح')),
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ رد شدن')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('rejected_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='رد کننده')),
('step_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.stepinstance', verbose_name='نمونه مرحله')),
],
options={
'verbose_name': 'historical رد شدن مرحله',
'verbose_name_plural': 'historical رد شدن\u200cهای مراحل',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='StepRejection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reason', models.TextField(help_text='توضیح کامل دلیل رد شدن', verbose_name='دلیل رد شدن')),
('instructions', models.TextField(blank=True, help_text='دستورالعمل\u200cهایی برای اصلاح مرحله', verbose_name='دستورالعمل\u200cهای اصلاح')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ رد شدن')),
('rejected_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_rejections', to=settings.AUTH_USER_MODEL, verbose_name='رد کننده')),
('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rejections', to='processes.stepinstance', verbose_name='نمونه مرحله')),
],
options={
'verbose_name': 'رد شدن مرحله',
'verbose_name_plural': 'رد شدن\u200cهای مراحل',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='HistoricalStepRevision',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')),
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='تاریخ اصلاح')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('revised_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='اصلاح کننده')),
('step_instance', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.stepinstance', verbose_name='نمونه مرحله')),
('rejection', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='processes.steprejection', verbose_name='رد شدن مربوطه')),
],
options={
'verbose_name': 'historical بازبینی مرحله',
'verbose_name_plural': 'historical بازبینی\u200cهای مراحل',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='StepRevision',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('changes_description', models.TextField(help_text='توضیح تغییراتی که برای اصلاح انجام شده', verbose_name='توضیح تغییرات')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ اصلاح')),
('rejection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='processes.steprejection', verbose_name='رد شدن مربوطه')),
('revised_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_revisions', to=settings.AUTH_USER_MODEL, verbose_name='اصلاح کننده')),
('step_instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='processes.stepinstance', verbose_name='نمونه مرحله')),
],
options={
'verbose_name': 'بازبینی مرحله',
'verbose_name_plural': 'بازبینی\u200cهای مراحل',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='StepDependency',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ایجاد')),
('dependency_step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependents', to='processes.processstep', verbose_name='مرحله مورد نیاز')),
('dependent_step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependencies', to='processes.processstep', verbose_name='مرحله وابسته')),
],
options={
'verbose_name': 'وابستگی مرحله',
'verbose_name_plural': 'وابستگی\u200cهای مراحل',
'ordering': ['dependent_step__order', 'dependency_step__order'],
'unique_together': {('dependent_step', 'dependency_step')},
},
),
]

View file

293
processes/models.py Normal file
View file

@ -0,0 +1,293 @@
from django.db import models
from django.contrib.auth import get_user_model
from common.models import NameSlugModel
from simple_history.models import HistoricalRecords
from django.core.exceptions import ValidationError
User = get_user_model()
class Process(NameSlugModel):
"""مدل فرآیند اصلی"""
description = models.TextField(verbose_name="توضیحات", blank=True)
is_active = models.BooleanField(default=True, verbose_name="فعال")
created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="ایجاد کننده")
history = HistoricalRecords()
class Meta:
verbose_name = "فرآیند"
verbose_name_plural = "فرآیندها"
ordering = ['-created']
def __str__(self):
return self.name
class ProcessStep(NameSlugModel):
"""مدل مراحل فرآیند"""
process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='steps', verbose_name="فرآیند")
order = models.PositiveIntegerField(verbose_name="ترتیب")
description = models.TextField(verbose_name="توضیحات", blank=True)
is_required = models.BooleanField(default=True, verbose_name="اجباری")
estimated_duration = models.PositiveIntegerField(verbose_name="مدت زمان تخمینی (روز)", null=True, blank=True)
# فیلدهای جدید برای کنترل وابستگی‌ها
blocks_previous = models.BooleanField(
default=False,
verbose_name="مسدود کننده مراحل قبلی",
help_text="اگر فعال باشد، پس از تکمیل این مرحله، مراحل قبلی غیرقابل ویرایش می‌شوند"
)
can_go_back = models.BooleanField(
default=True,
verbose_name="قابل بازگشت",
help_text="آیا می‌توان به مراحل قبلی بازگشت"
)
history = HistoricalRecords()
class Meta:
verbose_name = "مرحله فرآیند"
verbose_name_plural = "مراحل فرآیند"
ordering = ['process', 'order']
unique_together = ['process', 'order']
def __str__(self):
return f"{self.process.name} - {self.name}"
def get_dependencies(self):
"""دریافت مراحل وابسته"""
return StepDependency.objects.filter(dependent_step=self).values_list('dependency_step', flat=True)
def get_dependents(self):
"""دریافت مراحلی که به این مرحله وابسته هستند"""
return StepDependency.objects.filter(dependency_step=self).values_list('dependent_step', flat=True)
class StepDependency(models.Model):
"""مدل وابستگی بین مراحل"""
dependent_step = models.ForeignKey(
ProcessStep,
on_delete=models.CASCADE,
related_name='dependencies',
verbose_name="مرحله وابسته"
)
dependency_step = models.ForeignKey(
ProcessStep,
on_delete=models.CASCADE,
related_name='dependents',
verbose_name="مرحله مورد نیاز"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ ایجاد")
class Meta:
verbose_name = "وابستگی مرحله"
verbose_name_plural = "وابستگی‌های مراحل"
unique_together = ['dependent_step', 'dependency_step']
ordering = ['dependent_step__order', 'dependency_step__order']
def __str__(self):
return f"{self.dependent_step}{self.dependency_step}"
def clean(self):
"""اعتبارسنجی مدل"""
if self.dependent_step == self.dependency_step:
raise ValidationError("مرحله نمی‌تواند به خودش وابسته باشد")
if self.dependent_step.process != self.dependency_step.process:
raise ValidationError("مراحل باید از یک فرآیند باشند")
if self.dependent_step.order <= self.dependency_step.order:
raise ValidationError("مرحله وابسته باید بعد از مرحله مورد نیاز باشد")
class ProcessInstance(NameSlugModel):
"""مدل نمونه فرآیند (برای هر درخواست)"""
process = models.ForeignKey(Process, on_delete=models.CASCADE, related_name='instances', verbose_name="فرآیند")
requester = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="درخواست کننده")
current_step = models.ForeignKey('ProcessStep', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="مرحله فعلی")
status = models.CharField(
max_length=20,
choices=[
('pending', 'در انتظار'),
('in_progress', 'در حال انجام'),
('completed', 'تکمیل شده'),
('cancelled', 'لغو شده'),
('rejected', 'رد شده'),
],
default='pending',
verbose_name="وضعیت"
)
started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
history = HistoricalRecords()
class Meta:
verbose_name = "نمونه فرآیند"
verbose_name_plural = "نمونه‌های فرآیند"
ordering = ['-started_at']
def __str__(self):
return f"{self.process.name} - {self.requester.get_full_name()}"
def get_available_steps(self):
"""دریافت مراحل قابل دسترس"""
available_steps = []
for step in self.process.steps.all():
if self.can_access_step(step):
available_steps.append(step)
return available_steps
def can_access_step(self, step):
"""بررسی امکان دسترسی به مرحله"""
# بررسی وابستگی‌ها
dependencies = step.get_dependencies()
for dependency_id in dependencies:
step_instance = self.step_instances.filter(step_id=dependency_id).first()
if not step_instance or step_instance.status != 'completed':
return False
return True
def can_edit_step(self, step):
"""بررسی امکان ویرایش مرحله"""
# اگر مرحله مسدود کننده باشد و مراحل بعدی تکمیل شده باشند
if step.blocks_previous:
later_steps = self.step_instances.filter(
step__order__gt=step.order,
status='completed'
)
if later_steps.exists():
return False
return True
class StepInstance(models.Model):
"""مدل نمونه مرحله (برای هر مرحله در هر درخواست)"""
process_instance = models.ForeignKey(ProcessInstance, on_delete=models.CASCADE, related_name='step_instances', verbose_name="نمونه فرآیند")
step = models.ForeignKey(ProcessStep, on_delete=models.CASCADE, verbose_name="مرحله")
assigned_to = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="واگذار شده به")
status = models.CharField(
max_length=20,
choices=[
('pending', 'در انتظار'),
('in_progress', 'در حال انجام'),
('completed', 'تکمیل شده'),
('skipped', 'رد شده'),
('blocked', 'مسدود شده'),
('rejected', 'رد شده و نیاز به اصلاح'),
],
default='pending',
verbose_name="وضعیت"
)
notes = models.TextField(verbose_name="یادداشت‌ها", blank=True)
started_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ شروع")
completed_at = models.DateTimeField(null=True, blank=True, verbose_name="تاریخ تکمیل")
history = HistoricalRecords()
class Meta:
verbose_name = "نمونه مرحله"
verbose_name_plural = "نمونه‌های مرحله"
ordering = ['process_instance', 'step__order']
def __str__(self):
return f"{self.process_instance} - {self.step.name}"
def save(self, *args, **kwargs):
"""ذخیره با اعتبارسنجی"""
# بررسی وابستگی‌ها
if self.status == 'in_progress' or self.status == 'completed':
if not self.process_instance.can_access_step(self.step):
raise ValidationError("مراحل وابسته تکمیل نشده‌اند")
# بررسی امکان ویرایش
if self.status == 'completed' and not self.process_instance.can_edit_step(self.step):
raise ValidationError("این مرحله قابل ویرایش نیست")
super().save(*args, **kwargs)
def get_status_display_with_color(self):
"""نمایش وضعیت با رنگ"""
status_colors = {
'pending': 'secondary',
'in_progress': 'primary',
'completed': 'success',
'skipped': 'warning',
'blocked': 'danger',
'rejected': 'danger',
}
color = status_colors.get(self.status, 'secondary')
return '<span class="badge bg-{}">{}</span>'.format(color, self.get_status_display())
def get_rejection_count(self):
"""دریافت تعداد رد شدن‌ها"""
return self.rejections.count()
def get_latest_rejection(self):
"""دریافت آخرین رد شدن"""
return self.rejections.order_by('-created_at').first()
class StepRejection(models.Model):
"""مدل رد شدن مرحله"""
step_instance = models.ForeignKey(
StepInstance,
on_delete=models.CASCADE,
related_name='rejections',
verbose_name="نمونه مرحله"
)
rejected_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name="رد کننده",
related_name='step_rejections'
)
reason = models.TextField(verbose_name="دلیل رد شدن", help_text="توضیح کامل دلیل رد شدن")
instructions = models.TextField(
verbose_name="دستورالعمل‌های اصلاح",
help_text="دستورالعمل‌هایی برای اصلاح مرحله",
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ رد شدن")
history = HistoricalRecords()
class Meta:
verbose_name = "رد شدن مرحله"
verbose_name_plural = "رد شدن‌های مراحل"
ordering = ['-created_at']
def __str__(self):
return f"رد شدن {self.step_instance} توسط {self.rejected_by.get_full_name()}"
def save(self, *args, **kwargs):
"""ذخیره با تغییر وضعیت مرحله"""
# تغییر وضعیت مرحله به رد شده
self.step_instance.status = 'rejected'
self.step_instance.save()
super().save(*args, **kwargs)
class StepRevision(models.Model):
"""مدل بازبینی و اصلاح مرحله"""
step_instance = models.ForeignKey(
StepInstance,
on_delete=models.CASCADE,
related_name='revisions',
verbose_name="نمونه مرحله"
)
rejection = models.ForeignKey(
StepRejection,
on_delete=models.CASCADE,
related_name='revisions',
verbose_name="رد شدن مربوطه"
)
revised_by = models.ForeignKey(
User,
on_delete=models.CASCADE,
verbose_name="اصلاح کننده",
related_name='step_revisions'
)
changes_description = models.TextField(
verbose_name="توضیح تغییرات",
help_text="توضیح تغییراتی که برای اصلاح انجام شده"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="تاریخ اصلاح")
history = HistoricalRecords()
class Meta:
verbose_name = "بازبینی مرحله"
verbose_name_plural = "بازبینی‌های مراحل"
ordering = ['-created_at']
def __str__(self):
return f"بازبینی {self.step_instance} توسط {self.revised_by.get_full_name()}"

3
processes/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
processes/urls.py Normal file
View file

@ -0,0 +1,12 @@
from django.urls import path
from . import views
app_name = 'processes'
urlpatterns = [
path('', views.process_list, name='process_list'),
path('<int:process_id>/', views.process_detail, name='process_detail'),
path('<int:process_id>/start/', views.start_process, name='start_process'),
path('instance/<int:instance_id>/', views.instance_detail, name='instance_detail'),
path('my-processes/', views.my_processes, name='my_processes'),
]

79
processes/views.py Normal file
View file

@ -0,0 +1,79 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .models import Process, ProcessInstance, StepInstance
from .forms import ProcessInstanceForm
@login_required
def process_list(request):
"""نمایش لیست فرآیندهای فعال"""
processes = Process.objects.filter(is_active=True)
return render(request, 'processes/process_list.html', {
'processes': processes
})
@login_required
def process_detail(request, process_id):
"""نمایش جزئیات فرآیند"""
process = get_object_or_404(Process, id=process_id, is_active=True)
return render(request, 'processes/process_detail.html', {
'process': process
})
@login_required
def start_process(request, process_id):
"""شروع فرآیند جدید"""
process = get_object_or_404(Process, id=process_id, is_active=True)
if request.method == 'POST':
form = ProcessInstanceForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.process = process
instance.requester = request.user
instance.save()
# ایجاد نمونه‌های مرحله
for step in process.steps.all():
StepInstance.objects.create(
process_instance=instance,
step=step
)
# تنظیم مرحله اول به عنوان مرحله فعلی
first_step = process.steps.first()
if first_step:
instance.current_step = first_step
instance.status = 'in_progress'
instance.save()
messages.success(request, f'فرآیند {process.name} با موفقیت شروع شد.')
return redirect('processes:instance_detail', instance_id=instance.id)
else:
form = ProcessInstanceForm()
return render(request, 'processes/start_process.html', {
'process': process,
'form': form
})
@login_required
def instance_detail(request, instance_id):
"""نمایش جزئیات نمونه فرآیند"""
instance = get_object_or_404(ProcessInstance, id=instance_id)
return render(request, 'processes/instance_detail.html', {
'instance': instance
})
@login_required
def my_processes(request):
"""نمایش فرآیندهای کاربر"""
my_instances = ProcessInstance.objects.filter(requester=request.user)
assigned_steps = StepInstance.objects.filter(assigned_to=request.user, status='in_progress')
return render(request, 'processes/my_processes.html', {
'my_instances': my_instances,
'assigned_steps': assigned_steps
})

View file

@ -0,0 +1,279 @@
select.admin-autocomplete {
width: 20em;
}
.select2-container--admin-autocomplete.select2-container {
min-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single,
.select2-container--admin-autocomplete .select2-selection--multiple {
min-height: 30px;
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
border-color: var(--body-quiet-color);
min-height: 30px;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
padding: 0;
}
.select2-container--admin-autocomplete .select2-selection--single {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
color: var(--body-fg);
line-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
.select2-container--admin-autocomplete .select2-selection--multiple {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: text;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 10px 5px 5px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
list-style: none;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
color: var(--body-quiet-color);
margin-top: 5px;
float: left;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin: 5px;
position: absolute;
right: 0;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
background-color: var(--darkened-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
color: var(--body-quiet-color);
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
color: var(--body-fg);
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
border: solid var(--body-quiet-color) 1px;
outline: 0;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--admin-autocomplete .select2-search--dropdown {
background: var(--darkened-bg);
}
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
background: var(--body-bg);
color: var(--body-fg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
background: transparent;
color: var(--body-fg);
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield;
}
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
color: var(--body-fg);
background: var(--body-bg);
}
.select2-container--admin-autocomplete .select2-results__option[role=group] {
padding: 0;
}
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
background-color: var(--selected-bg);
color: var(--body-fg);
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
padding-left: 1em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em;
}
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary);
color: var(--primary-fg);
}
.select2-container--admin-autocomplete .select2-results__group {
cursor: default;
display: block;
padding: 6px;
}
.errors .select2-selection {
border: 1px solid var(--error-fg);
}

1179
static/admin/css/base.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,343 @@
/* CHANGELISTS */
#changelist {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
#changelist .changelist-form-container {
flex: 1 1 auto;
min-width: 0;
}
#changelist table {
width: 100%;
}
.change-list .hiddenfields { display:none; }
.change-list .filtered table {
border-right: none;
}
.change-list .filtered {
min-height: 400px;
}
.change-list .filtered .results, .change-list .filtered .paginator,
.filtered #toolbar, .filtered div.xfull {
width: auto;
}
.change-list .filtered table tbody th {
padding-right: 1em;
}
#changelist-form .results {
overflow-x: auto;
width: 100%;
}
#changelist .toplinks {
border-bottom: 1px solid var(--hairline-color);
}
#changelist .paginator {
color: var(--body-quiet-color);
border-bottom: 1px solid var(--hairline-color);
background: var(--body-bg);
overflow: hidden;
}
/* CHANGELIST TABLES */
#changelist table thead th {
padding: 0;
white-space: nowrap;
vertical-align: middle;
}
#changelist table thead th.action-checkbox-column {
width: 1.5em;
text-align: center;
}
#changelist table tbody td.action-checkbox {
text-align: center;
}
#changelist table tfoot {
color: var(--body-quiet-color);
}
/* TOOLBAR */
#toolbar {
padding: 8px 10px;
margin-bottom: 15px;
border-top: 1px solid var(--hairline-color);
border-bottom: 1px solid var(--hairline-color);
background: var(--darkened-bg);
color: var(--body-quiet-color);
}
#toolbar form input {
border-radius: 4px;
font-size: 0.875rem;
padding: 5px;
color: var(--body-fg);
}
#toolbar #searchbar {
height: 1.1875rem;
border: 1px solid var(--border-color);
padding: 2px 5px;
margin: 0;
vertical-align: top;
font-size: 0.8125rem;
max-width: 100%;
}
#toolbar #searchbar:focus {
border-color: var(--body-quiet-color);
}
#toolbar form input[type="submit"] {
border: 1px solid var(--border-color);
font-size: 0.8125rem;
padding: 4px 8px;
margin: 0;
vertical-align: middle;
background: var(--body-bg);
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
cursor: pointer;
color: var(--body-fg);
}
#toolbar form input[type="submit"]:focus,
#toolbar form input[type="submit"]:hover {
border-color: var(--body-quiet-color);
}
#changelist-search img {
vertical-align: middle;
margin-right: 4px;
}
#changelist-search .help {
word-break: break-word;
}
/* FILTER COLUMN */
#changelist-filter {
flex: 0 0 240px;
order: 1;
background: var(--darkened-bg);
border-left: none;
margin: 0 0 0 30px;
}
@media (forced-colors: active) {
#changelist-filter {
border: 1px solid;
}
}
#changelist-filter h2 {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 5px 15px;
margin-bottom: 12px;
border-bottom: none;
}
#changelist-filter h3,
#changelist-filter details summary {
font-weight: 400;
padding: 0 15px;
margin-bottom: 10px;
}
#changelist-filter details summary > * {
display: inline;
}
#changelist-filter details > summary {
list-style-type: none;
}
#changelist-filter details > summary::-webkit-details-marker {
display: none;
}
#changelist-filter details > summary::before {
content: '→';
font-weight: bold;
color: var(--link-hover-color);
}
#changelist-filter details[open] > summary::before {
content: '↓';
}
#changelist-filter ul {
margin: 5px 0;
padding: 0 15px 15px;
border-bottom: 1px solid var(--hairline-color);
}
#changelist-filter ul:last-child {
border-bottom: none;
}
#changelist-filter li {
list-style-type: none;
margin-left: 0;
padding-left: 0;
}
#changelist-filter a {
display: block;
color: var(--body-quiet-color);
word-break: break-word;
}
#changelist-filter li.selected {
border-left: 5px solid var(--hairline-color);
padding-left: 10px;
margin-left: -15px;
}
#changelist-filter li.selected a {
color: var(--link-selected-fg);
}
#changelist-filter a:focus, #changelist-filter a:hover,
#changelist-filter li.selected a:focus,
#changelist-filter li.selected a:hover {
color: var(--link-hover-color);
}
#changelist-filter #changelist-filter-extra-actions {
font-size: 0.8125rem;
margin-bottom: 10px;
border-bottom: 1px solid var(--hairline-color);
}
/* DATE DRILLDOWN */
.change-list .toplinks {
display: flex;
padding-bottom: 5px;
flex-wrap: wrap;
gap: 3px 17px;
font-weight: bold;
}
.change-list .toplinks a {
font-size: 0.8125rem;
}
.change-list .toplinks .date-back {
color: var(--body-quiet-color);
}
.change-list .toplinks .date-back:focus,
.change-list .toplinks .date-back:hover {
color: var(--link-hover-color);
}
/* ACTIONS */
.filtered .actions {
border-right: none;
}
#changelist table input {
margin: 0;
vertical-align: baseline;
}
/* Once the :has() pseudo-class is supported by all browsers, the tr.selected
selector and the JS adding the class can be removed. */
#changelist tbody tr.selected {
background-color: var(--selected-row);
}
#changelist tbody tr:has(.action-select:checked) {
background-color: var(--selected-row);
}
@media (forced-colors: active) {
#changelist tbody tr.selected {
background-color: SelectedItem;
}
#changelist tbody tr:has(.action-select:checked) {
background-color: SelectedItem;
}
}
#changelist .actions {
padding: 10px;
background: var(--body-bg);
border-top: none;
border-bottom: none;
line-height: 1.5rem;
color: var(--body-quiet-color);
width: 100%;
}
#changelist .actions span.all,
#changelist .actions span.action-counter,
#changelist .actions span.clear,
#changelist .actions span.question {
font-size: 0.8125rem;
margin: 0 0.5em;
}
#changelist .actions:last-child {
border-bottom: none;
}
#changelist .actions select {
vertical-align: top;
height: 1.5rem;
color: var(--body-fg);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.875rem;
padding: 0 0 0 4px;
margin: 0;
margin-left: 10px;
}
#changelist .actions select:focus {
border-color: var(--body-quiet-color);
}
#changelist .actions label {
display: inline-block;
vertical-align: middle;
font-size: 0.8125rem;
}
#changelist .actions .button {
font-size: 0.8125rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--body-bg);
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
cursor: pointer;
height: 1.5rem;
line-height: 1;
padding: 4px 8px;
margin: 0;
color: var(--body-fg);
}
#changelist .actions .button:focus, #changelist .actions .button:hover {
border-color: var(--body-quiet-color);
}

View file

@ -0,0 +1,18 @@
@import "/static/dist/css/Vazirmatn-font-face.css";
@font-face {
font-family: 'Sahel';
src: url('/static/fonts/Sahel-FD.eot');
src: url('/static/fonts/Sahel-FD.eot?#iefix') format('embedded-opentype'),
url('/static/fonts/Sahel-FD.woff') format('woff'),
url('/static/fonts/Sahel-FD.ttf') format('truetype');
font-weight: normal;
}
[dir="rtl"],
[dir="rtl"] * {
font-family: 'Sahel', /*'Vazirmatn', 'Vazir',*/ "Font Awesome 6 Free",
"Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol" !important;
}

View file

@ -0,0 +1,130 @@
@media (prefers-color-scheme: dark) {
:root {
--primary: #264b5d;
--primary-fg: #f7f7f7;
--body-fg: #eeeeee;
--body-bg: #121212;
--body-quiet-color: #d0d0d0;
--body-medium-color: #e0e0e0;
--body-loud-color: #ffffff;
--breadcrumbs-link-fg: #e0e0e0;
--breadcrumbs-bg: var(--primary);
--link-fg: #81d4fa;
--link-hover-color: #4ac1f7;
--link-selected-fg: #6f94c6;
--hairline-color: #272727;
--border-color: #353535;
--error-fg: #e35f5f;
--message-success-bg: #006b1b;
--message-warning-bg: #583305;
--message-error-bg: #570808;
--darkened-bg: #212121;
--selected-bg: #1b1b1b;
--selected-row: #00363a;
--close-button-bg: #333333;
--close-button-hover-bg: #666666;
color-scheme: dark;
}
}
html[data-theme="dark"] {
--primary: #264b5d;
--primary-fg: #f7f7f7;
--body-fg: #eeeeee;
--body-bg: #121212;
--body-quiet-color: #d0d0d0;
--body-medium-color: #e0e0e0;
--body-loud-color: #ffffff;
--breadcrumbs-link-fg: #e0e0e0;
--breadcrumbs-bg: var(--primary);
--link-fg: #81d4fa;
--link-hover-color: #4ac1f7;
--link-selected-fg: #6f94c6;
--hairline-color: #272727;
--border-color: #353535;
--error-fg: #e35f5f;
--message-success-bg: #006b1b;
--message-warning-bg: #583305;
--message-error-bg: #570808;
--darkened-bg: #212121;
--selected-bg: #1b1b1b;
--selected-row: #00363a;
--close-button-bg: #333333;
--close-button-hover-bg: #666666;
color-scheme: dark;
}
/* THEME SWITCH */
.theme-toggle {
cursor: pointer;
border: none;
padding: 0;
background: transparent;
vertical-align: middle;
margin-inline-start: 5px;
margin-top: -1px;
}
.theme-toggle svg {
vertical-align: middle;
height: 1rem;
width: 1rem;
display: none;
}
/*
Fully hide screen reader text so we only show the one matching the current
theme.
*/
.theme-toggle .visually-hidden {
display: none;
}
html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
display: block;
}
html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
display: block;
}
html[data-theme="light"] .theme-toggle .theme-label-when-light {
display: block;
}
/* ICONS */
.theme-toggle svg.theme-icon-when-auto,
.theme-toggle svg.theme-icon-when-dark,
.theme-toggle svg.theme-icon-when-light {
fill: var(--header-link-color);
color: var(--header-bg);
}
html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
display: block;
}
html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
display: block;
}
html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
display: block;
}

View file

@ -0,0 +1,29 @@
/* DASHBOARD */
.dashboard td, .dashboard th {
word-break: break-word;
}
.dashboard .module table th {
width: 100%;
}
.dashboard .module table td {
white-space: nowrap;
}
.dashboard .module table td a {
display: block;
padding-right: .6em;
}
/* RECENT ACTIONS MODULE */
.module ul.actionlist {
margin-left: 0;
}
ul.actionlist li {
list-style-type: none;
overflow: hidden;
text-overflow: ellipsis;
}

512
static/admin/css/forms.css Normal file
View file

@ -0,0 +1,512 @@
@import url('widgets.css');
/* FORM ROWS */
.form-row {
overflow: hidden;
padding: 10px;
font-size: 0.8125rem;
border-bottom: 1px solid var(--hairline-color);
}
.form-row img, .form-row input {
vertical-align: middle;
}
.form-row label input[type="checkbox"] {
margin-top: 0;
vertical-align: 0;
}
form .form-row p {
padding-left: 0;
}
.flex-container {
display: flex;
}
.form-multiline {
flex-wrap: wrap;
}
.form-multiline > div {
padding-bottom: 10px;
}
/* FORM LABELS */
label {
font-weight: normal;
color: var(--body-quiet-color);
font-size: 0.8125rem;
}
.required label, label.required {
font-weight: bold;
}
/* RADIO BUTTONS */
form div.radiolist div {
padding-right: 7px;
}
form div.radiolist.inline div {
display: inline-block;
}
form div.radiolist label {
width: auto;
}
form div.radiolist input[type="radio"] {
margin: -2px 4px 0 0;
padding: 0;
}
form ul.inline {
margin-left: 0;
padding: 0;
}
form ul.inline li {
float: left;
padding-right: 7px;
}
/* FIELDSETS */
fieldset .fieldset-heading,
fieldset .inline-heading,
:not(.inline-related) .collapse summary {
border: 1px solid var(--header-bg);
margin: 0;
padding: 8px;
font-weight: 400;
font-size: 0.8125rem;
background: var(--header-bg);
color: var(--header-link-color);
}
/* ALIGNED FIELDSETS */
.aligned label {
display: block;
padding: 4px 10px 0 0;
min-width: 160px;
width: 160px;
word-wrap: break-word;
}
.aligned label:not(.vCheckboxLabel):after {
content: '';
display: inline-block;
vertical-align: middle;
}
.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
padding: 6px 0;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
overflow-wrap: break-word;
}
.aligned ul label {
display: inline;
float: none;
width: auto;
}
.aligned .form-row input {
margin-bottom: 0;
}
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
width: 350px;
}
form .aligned ul {
margin-left: 160px;
padding-left: 10px;
}
form .aligned div.radiolist {
display: inline-block;
margin: 0;
padding: 0;
}
form .aligned p.help,
form .aligned div.help {
margin-top: 0;
margin-left: 160px;
padding-left: 10px;
}
form .aligned p.date div.help.timezonewarning,
form .aligned p.datetime div.help.timezonewarning,
form .aligned p.time div.help.timezonewarning {
margin-left: 0;
padding-left: 0;
font-weight: normal;
}
form .aligned p.help:last-child,
form .aligned div.help:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
form .aligned input + p.help,
form .aligned textarea + p.help,
form .aligned select + p.help,
form .aligned input + div.help,
form .aligned textarea + div.help,
form .aligned select + div.help {
margin-left: 160px;
padding-left: 10px;
}
form .aligned select option:checked {
background-color: var(--selected-row);
}
form .aligned ul li {
list-style: none;
}
form .aligned table p {
margin-left: 0;
padding-left: 0;
}
.aligned .vCheckboxLabel {
padding: 1px 0 0 5px;
}
.aligned .vCheckboxLabel + p.help,
.aligned .vCheckboxLabel + div.help {
margin-top: -4px;
}
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
width: 610px;
}
fieldset .fieldBox {
margin-right: 20px;
}
/* WIDE FIELDSETS */
.wide label {
width: 200px;
}
form .wide p.help,
form .wide ul.errorlist,
form .wide div.help {
padding-left: 50px;
}
form div.help ul {
padding-left: 0;
margin-left: 0;
}
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
width: 450px;
}
/* COLLAPSIBLE FIELDSETS */
.collapse summary .fieldset-heading,
.collapse summary .inline-heading {
background: transparent;
border: none;
color: currentColor;
display: inline;
margin: 0;
padding: 0;
}
/* MONOSPACE TEXTAREAS */
fieldset.monospace textarea {
font-family: var(--font-family-monospace);
}
/* SUBMIT ROW */
.submit-row {
padding: 12px 14px 12px;
margin: 0 0 20px;
background: var(--darkened-bg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
overflow: hidden;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
body.popup .submit-row {
overflow: auto;
}
.submit-row input {
height: 2.1875rem;
line-height: 0.9375rem;
}
.submit-row input, .submit-row a {
margin: 0;
}
.submit-row input.default {
text-transform: uppercase;
}
.submit-row a.deletelink {
margin-left: auto;
}
.submit-row a.deletelink {
display: block;
background: var(--delete-button-bg);
border-radius: 4px;
padding: 0.625rem 0.9375rem;
height: 0.9375rem;
line-height: 0.9375rem;
color: var(--button-fg);
}
.submit-row a.closelink {
display: inline-block;
background: var(--close-button-bg);
border-radius: 4px;
padding: 10px 15px;
height: 0.9375rem;
line-height: 0.9375rem;
color: var(--button-fg);
}
.submit-row a.deletelink:focus,
.submit-row a.deletelink:hover,
.submit-row a.deletelink:active {
background: var(--delete-button-hover-bg);
text-decoration: none;
}
.submit-row a.closelink:focus,
.submit-row a.closelink:hover,
.submit-row a.closelink:active {
background: var(--close-button-hover-bg);
text-decoration: none;
}
/* CUSTOM FORM FIELDS */
.vSelectMultipleField {
vertical-align: top;
}
.vCheckboxField {
border: none;
}
.vDateField, .vTimeField {
margin-right: 2px;
margin-bottom: 4px;
}
.vDateField {
min-width: 6.85em;
}
.vTimeField {
min-width: 4.7em;
}
.vURLField {
width: 30em;
}
.vLargeTextField, .vXMLLargeTextField {
width: 48em;
}
.flatpages-flatpage #id_content {
height: 40.2em;
}
.module table .vPositiveSmallIntegerField {
width: 2.2em;
}
.vIntegerField {
width: 5em;
}
.vBigIntegerField {
width: 10em;
}
.vForeignKeyRawIdAdminField {
width: 5em;
}
.vTextField, .vUUIDField {
width: 20em;
}
/* INLINES */
.inline-group {
padding: 0;
margin: 0 0 30px;
}
.inline-group thead th {
padding: 8px 10px;
}
.inline-group .aligned label {
width: 160px;
}
.inline-related {
position: relative;
}
.inline-related h4,
.inline-related:not(.tabular) .collapse summary {
margin: 0;
color: var(--body-medium-color);
padding: 5px;
font-size: 0.8125rem;
background: var(--darkened-bg);
border: 1px solid var(--hairline-color);
border-left-color: var(--darkened-bg);
border-right-color: var(--darkened-bg);
}
.inline-related h3 span.delete {
float: right;
}
.inline-related h3 span.delete label {
margin-left: 2px;
font-size: 0.6875rem;
}
.inline-related fieldset {
margin: 0;
background: var(--body-bg);
border: none;
width: 100%;
}
.inline-group .tabular fieldset.module {
border: none;
}
.inline-related.tabular fieldset.module table {
width: 100%;
overflow-x: scroll;
}
.last-related fieldset {
border: none;
}
.inline-group .tabular tr.has_original td {
padding-top: 2em;
}
.inline-group .tabular tr td.original {
padding: 2px 0 0 0;
width: 0;
_position: relative;
}
.inline-group .tabular th.original {
width: 0px;
padding: 0;
}
.inline-group .tabular td.original p {
position: absolute;
left: 0;
height: 1.1em;
padding: 2px 9px;
overflow: hidden;
font-size: 0.5625rem;
font-weight: bold;
color: var(--body-quiet-color);
_width: 700px;
}
.inline-group ul.tools {
padding: 0;
margin: 0;
list-style: none;
}
.inline-group ul.tools li {
display: inline;
padding: 0 5px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
color: var(--body-quiet-color);
background: var(--darkened-bg);
padding: 8px 10px;
border-bottom: 1px solid var(--hairline-color);
}
.inline-group .tabular tr.add-row td {
padding: 8px 10px;
border-bottom: 1px solid var(--hairline-color);
}
.inline-group ul.tools a.add,
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
padding-left: 16px;
font-size: 0.75rem;
}
.empty-form {
display: none;
}
/* RELATED FIELD ADD ONE / LOOKUP */
.related-lookup {
margin-left: 5px;
display: inline-block;
vertical-align: middle;
background-repeat: no-repeat;
background-size: 14px;
}
.related-lookup {
width: 1rem;
height: 1rem;
background-image: url(../img/search.svg);
}
form .related-widget-wrapper ul {
display: inline-block;
margin-left: 0;
padding-left: 0;
}
.clearable-file-input input {
margin-top: 0;
}

View file

@ -0,0 +1,61 @@
/* LOGIN FORM */
.login {
background: var(--darkened-bg);
height: auto;
}
.login #header {
height: auto;
padding: 15px 16px;
justify-content: center;
}
.login #header h1 {
font-size: 1.125rem;
margin: 0;
}
.login #header h1 a {
color: var(--header-link-color);
}
.login #content {
padding: 20px;
}
.login #container {
background: var(--body-bg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
overflow: hidden;
width: 28em;
min-width: 300px;
margin: 100px auto;
height: auto;
}
.login .form-row {
padding: 4px 0;
}
.login .form-row label {
display: block;
line-height: 2em;
}
.login .form-row #id_username, .login .form-row #id_password {
padding: 8px;
width: 100%;
box-sizing: border-box;
}
.login .submit-row {
padding: 1em 0 0 0;
margin: 0;
text-align: center;
}
.login .password-reset-link {
text-align: center;
}

View file

@ -0,0 +1,150 @@
.sticky {
position: sticky;
top: 0;
max-height: 100vh;
}
.toggle-nav-sidebar {
z-index: 20;
left: 0;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 23px;
width: 23px;
border: 0;
border-right: 1px solid var(--hairline-color);
background-color: var(--body-bg);
cursor: pointer;
font-size: 1.25rem;
color: var(--link-fg);
padding: 0;
}
[dir="rtl"] .toggle-nav-sidebar {
border-left: 1px solid var(--hairline-color);
border-right: 0;
}
.toggle-nav-sidebar:hover,
.toggle-nav-sidebar:focus {
background-color: var(--darkened-bg);
}
#nav-sidebar {
z-index: 15;
flex: 0 0 275px;
left: -276px;
margin-left: -276px;
border-top: 1px solid transparent;
border-right: 1px solid var(--hairline-color);
background-color: var(--body-bg);
overflow: auto;
}
[dir="rtl"] #nav-sidebar {
border-left: 1px solid var(--hairline-color);
border-right: 0;
left: 0;
margin-left: 0;
right: -276px;
margin-right: -276px;
}
.toggle-nav-sidebar::before {
content: '\00BB';
}
.main.shifted .toggle-nav-sidebar::before {
content: '\00AB';
}
.main > #nav-sidebar {
visibility: hidden;
}
.main.shifted > #nav-sidebar {
margin-left: 0;
visibility: visible;
}
[dir="rtl"] .main.shifted > #nav-sidebar {
margin-right: 0;
}
#nav-sidebar .module th {
width: 100%;
overflow-wrap: anywhere;
}
#nav-sidebar .module th,
#nav-sidebar .module caption {
padding-left: 16px;
}
#nav-sidebar .module td {
white-space: nowrap;
}
[dir="rtl"] #nav-sidebar .module th,
[dir="rtl"] #nav-sidebar .module caption {
padding-left: 8px;
padding-right: 16px;
}
#nav-sidebar .current-app .section:link,
#nav-sidebar .current-app .section:visited {
color: var(--header-color);
font-weight: bold;
}
#nav-sidebar .current-model {
background: var(--selected-row);
}
@media (forced-colors: active) {
#nav-sidebar .current-model {
background-color: SelectedItem;
}
}
.main > #nav-sidebar + .content {
max-width: calc(100% - 23px);
}
.main.shifted > #nav-sidebar + .content {
max-width: calc(100% - 299px);
}
@media (max-width: 767px) {
#nav-sidebar, #toggle-nav-sidebar {
display: none;
}
.main > #nav-sidebar + .content,
.main.shifted > #nav-sidebar + .content {
max-width: 100%;
}
}
#nav-filter {
width: 100%;
box-sizing: border-box;
padding: 2px 5px;
margin: 5px 0;
border: 1px solid var(--border-color);
background-color: var(--darkened-bg);
color: var(--body-fg);
}
#nav-filter:focus {
border-color: var(--body-quiet-color);
}
#nav-filter.no-results {
background: var(--message-error-bg);
}
#nav-sidebar table {
width: 100%;
}

View file

@ -0,0 +1,967 @@
/* Tablets */
input[type="submit"], button {
-webkit-appearance: none;
appearance: none;
}
@media (max-width: 1024px) {
/* Basic */
html {
-webkit-text-size-adjust: 100%;
}
td, th {
padding: 10px;
font-size: 0.875rem;
}
.small {
font-size: 0.75rem;
}
/* Layout */
#container {
min-width: 0;
}
#content {
padding: 15px 20px 20px;
}
div.breadcrumbs {
padding: 10px 30px;
}
/* Header */
#header {
flex-direction: column;
padding: 15px 30px;
justify-content: flex-start;
}
#site-name {
margin: 0 0 8px;
line-height: 1.2;
}
#user-tools {
margin: 0;
font-weight: 400;
line-height: 1.85;
text-align: left;
}
#user-tools a {
display: inline-block;
line-height: 1.4;
}
/* Dashboard */
.dashboard #content {
width: auto;
}
#content-related {
margin-right: -290px;
}
.colSM #content-related {
margin-left: -290px;
}
.colMS {
margin-right: 290px;
}
.colSM {
margin-left: 290px;
}
.dashboard .module table td a {
padding-right: 0;
}
td .changelink, td .addlink {
font-size: 0.8125rem;
}
/* Changelist */
#toolbar {
border: none;
padding: 15px;
}
#changelist-search > div {
display: flex;
flex-wrap: nowrap;
max-width: 480px;
}
#changelist-search label {
line-height: 1.375rem;
}
#toolbar form #searchbar {
flex: 1 0 auto;
width: 0;
height: 1.375rem;
margin: 0 10px 0 6px;
}
#toolbar form input[type=submit] {
flex: 0 1 auto;
}
#changelist-search .quiet {
width: 0;
flex: 1 0 auto;
margin: 5px 0 0 25px;
}
#changelist .actions {
display: flex;
flex-wrap: wrap;
padding: 15px 0;
}
#changelist .actions label {
display: flex;
}
#changelist .actions select {
background: var(--body-bg);
}
#changelist .actions .button {
min-width: 48px;
margin: 0 10px;
}
#changelist .actions span.all,
#changelist .actions span.clear,
#changelist .actions span.question,
#changelist .actions span.action-counter {
font-size: 0.6875rem;
margin: 0 10px 0 0;
}
#changelist-filter {
flex-basis: 200px;
}
.change-list .filtered .results,
.change-list .filtered .paginator,
.filtered #toolbar,
.filtered .actions,
#changelist .paginator {
border-top-color: var(--hairline-color); /* XXX Is this used at all? */
}
#changelist .results + .paginator {
border-top: none;
}
/* Forms */
label {
font-size: 1rem;
}
/*
Minifiers remove the default (text) "type" attribute from "input" HTML
tags. Add input:not([type]) to make the CSS stylesheet work the same.
*/
.form-row input:not([type]),
.form-row input[type=text],
.form-row input[type=password],
.form-row input[type=email],
.form-row input[type=url],
.form-row input[type=tel],
.form-row input[type=number],
.form-row textarea,
.form-row select,
.form-row .vTextField {
box-sizing: border-box;
margin: 0;
padding: 6px 8px;
min-height: 2.25rem;
font-size: 1rem;
}
.form-row select {
height: 2.25rem;
}
.form-row select[multiple] {
height: auto;
min-height: 0;
}
fieldset .fieldBox + .fieldBox {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--hairline-color);
}
textarea {
max-width: 100%;
max-height: 120px;
}
.aligned label {
padding-top: 6px;
}
.aligned .related-lookup,
.aligned .datetimeshortcuts,
.aligned .related-lookup + strong {
align-self: center;
margin-left: 15px;
}
form .aligned div.radiolist {
margin-left: 2px;
}
.submit-row {
padding: 8px;
}
.submit-row a.deletelink {
padding: 10px 7px;
}
.button, input[type=submit], input[type=button], .submit-row input, a.button {
padding: 7px;
}
/* Selector */
.selector {
display: flex;
width: 100%;
}
.selector .selector-filter {
display: flex;
align-items: center;
}
.selector .selector-filter label {
margin: 0 8px 0 0;
}
.selector .selector-filter input {
width: 100%;
min-height: 0;
flex: 1 1;
}
.selector-available, .selector-chosen {
width: auto;
flex: 1 1;
display: flex;
flex-direction: column;
}
.selector select {
width: 100%;
flex: 1 0 auto;
margin-bottom: 5px;
}
.selector ul.selector-chooser {
width: 26px;
height: 52px;
padding: 2px 0;
border-radius: 20px;
transform: translateY(-10px);
}
.selector-add, .selector-remove {
width: 20px;
height: 20px;
background-size: 20px auto;
}
.selector-add {
background-position: 0 -120px;
}
.selector-remove {
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
align-self: center;
}
.stacked {
flex-direction: column;
max-width: 480px;
}
.stacked > * {
flex: 0 1 auto;
}
.stacked select {
margin-bottom: 0;
}
.stacked .selector-available, .stacked .selector-chosen {
width: auto;
}
.stacked ul.selector-chooser {
width: 52px;
height: 26px;
padding: 0 2px;
transform: none;
}
.stacked .selector-chooser li {
padding: 3px;
}
.stacked .selector-add, .stacked .selector-remove {
background-size: 20px auto;
}
.stacked .selector-add {
background-position: 0 -40px;
}
.stacked .active.selector-add {
background-position: 0 -40px;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -140px;
}
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
background-position: 0 -60px;
}
.stacked .selector-remove {
background-position: 0 0;
}
.stacked .active.selector-remove {
background-position: 0 0;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -100px;
}
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
background-position: 0 -20px;
}
.help-tooltip, .selector .help-icon {
display: none;
}
.datetime input {
width: 50%;
max-width: 120px;
}
.datetime span {
font-size: 0.8125rem;
}
.datetime .timezonewarning {
display: block;
font-size: 0.6875rem;
color: var(--body-quiet-color);
}
.datetimeshortcuts {
color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */
}
.form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
width: 75%;
}
.inline-group {
overflow: auto;
}
/* Messages */
ul.messagelist li {
padding-left: 55px;
background-position: 30px 12px;
}
ul.messagelist li.error {
background-position: 30px 12px;
}
ul.messagelist li.warning {
background-position: 30px 14px;
}
/* Login */
.login #header {
padding: 15px 20px;
}
.login #site-name {
margin: 0;
}
/* GIS */
div.olMap {
max-width: calc(100vw - 30px);
max-height: 300px;
}
.olMap + .clear_features {
display: block;
margin-top: 10px;
}
/* Docs */
.module table.xfull {
width: 100%;
}
pre.literal-block {
overflow: auto;
}
}
/* Mobile */
@media (max-width: 767px) {
/* Layout */
#header, #content {
padding: 15px;
}
div.breadcrumbs {
padding: 10px 15px;
}
/* Dashboard */
.colMS, .colSM {
margin: 0;
}
#content-related, .colSM #content-related {
width: 100%;
margin: 0;
}
#content-related .module {
margin-bottom: 0;
}
#content-related .module h2 {
padding: 10px 15px;
font-size: 1rem;
}
/* Changelist */
#changelist {
align-items: stretch;
flex-direction: column;
}
#toolbar {
padding: 10px;
}
#changelist-filter {
margin-left: 0;
}
#changelist .actions label {
flex: 1 1;
}
#changelist .actions select {
flex: 1 0;
width: 100%;
}
#changelist .actions span {
flex: 1 0 100%;
}
#changelist-filter {
position: static;
width: auto;
margin-top: 30px;
}
.object-tools {
float: none;
margin: 0 0 15px;
padding: 0;
overflow: hidden;
}
.object-tools li {
height: auto;
margin-left: 0;
}
.object-tools li + li {
margin-left: 15px;
}
/* Forms */
.form-row {
padding: 15px 0;
}
.aligned .form-row,
.aligned .form-row > div {
max-width: 100vw;
}
.aligned .form-row > div {
width: calc(100vw - 30px);
}
.flex-container {
flex-flow: column;
}
.flex-container.checkbox-row {
flex-flow: row;
}
textarea {
max-width: none;
}
.vURLField {
width: auto;
}
fieldset .fieldBox + .fieldBox {
margin-top: 15px;
padding-top: 15px;
}
.aligned label {
width: 100%;
min-width: auto;
padding: 0 0 10px;
}
.aligned label:after {
max-height: 0;
}
.aligned .form-row input,
.aligned .form-row select,
.aligned .form-row textarea {
flex: 1 1 auto;
max-width: 100%;
}
.aligned .checkbox-row input {
flex: 0 1 auto;
margin: 0;
}
.aligned .vCheckboxLabel {
flex: 1 0;
padding: 1px 0 0 5px;
}
.aligned label + p,
.aligned label + div.help,
.aligned label + div.readonly {
padding: 0;
margin-left: 0;
}
.aligned p.file-upload {
font-size: 0.8125rem;
}
span.clearable-file-input {
margin-left: 15px;
}
span.clearable-file-input label {
font-size: 0.8125rem;
padding-bottom: 0;
}
.aligned .timezonewarning {
flex: 1 0 100%;
margin-top: 5px;
}
form .aligned .form-row div.help {
width: 100%;
margin: 5px 0 0;
padding: 0;
}
form .aligned ul,
form .aligned ul.errorlist {
margin-left: 0;
padding-left: 0;
}
form .aligned div.radiolist {
margin-top: 5px;
margin-right: 15px;
margin-bottom: -3px;
}
form .aligned div.radiolist:not(.inline) div + div {
margin-top: 5px;
}
/* Related widget */
.related-widget-wrapper {
width: 100%;
display: flex;
align-items: flex-start;
}
.related-widget-wrapper .selector {
order: 1;
}
.related-widget-wrapper > a {
order: 2;
}
.related-widget-wrapper .radiolist ~ a {
align-self: flex-end;
}
.related-widget-wrapper > select ~ a {
align-self: center;
}
/* Selector */
.selector {
flex-direction: column;
gap: 10px 0;
}
.selector-available, .selector-chosen {
flex: 1 1 auto;
}
.selector select {
max-height: 96px;
}
.selector ul.selector-chooser {
display: block;
width: 52px;
height: 26px;
padding: 0 2px;
transform: none;
}
.selector ul.selector-chooser li {
float: left;
}
.selector-remove {
background-position: 0 0;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -20px;
}
.selector-add {
background-position: 0 -40px;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -60px;
}
/* Inlines */
.inline-group[data-inline-type="stacked"] .inline-related {
border: 1px solid var(--hairline-color);
border-radius: 4px;
margin-top: 15px;
overflow: auto;
}
.inline-group[data-inline-type="stacked"] .inline-related > * {
box-sizing: border-box;
}
.inline-group[data-inline-type="stacked"] .inline-related .module {
padding: 0 10px;
}
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row {
border-top: 1px solid var(--hairline-color);
border-bottom: none;
}
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child {
border-top: none;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 {
padding: 10px;
border-top-width: 0;
border-bottom-width: 2px;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label {
margin-right: auto;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 span.delete {
float: none;
flex: 1 1 100%;
margin-top: 5px;
}
.inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) {
width: 100%;
}
.inline-group[data-inline-type="stacked"] .aligned label {
width: 100%;
}
.inline-group[data-inline-type="stacked"] div.add-row {
margin-top: 15px;
border: 1px solid var(--hairline-color);
border-radius: 4px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
padding: 0;
}
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
display: block;
padding: 8px 10px 8px 26px;
background-position: 8px 9px;
}
/* Submit row */
.submit-row {
padding: 10px;
margin: 0 0 15px;
flex-direction: column;
gap: 8px;
}
.submit-row input, .submit-row input.default, .submit-row a {
text-align: center;
}
.submit-row a.closelink {
padding: 10px 0;
text-align: center;
}
.submit-row a.deletelink {
margin: 0;
}
/* Messages */
ul.messagelist li {
padding-left: 40px;
background-position: 15px 12px;
}
ul.messagelist li.error {
background-position: 15px 12px;
}
ul.messagelist li.warning {
background-position: 15px 14px;
}
/* Paginator */
.paginator .this-page, .paginator a:link, .paginator a:visited {
padding: 4px 10px;
}
/* Login */
body.login {
padding: 0 15px;
}
.login #container {
width: auto;
max-width: 480px;
margin: 50px auto;
}
.login #header,
.login #content {
padding: 15px;
}
.login #content-main {
float: none;
}
.login .form-row {
padding: 0;
}
.login .form-row + .form-row {
margin-top: 15px;
}
.login .form-row label {
margin: 0 0 5px;
line-height: 1.2;
}
.login .submit-row {
padding: 15px 0 0;
}
.login br {
display: none;
}
.login .submit-row input {
margin: 0;
text-transform: uppercase;
}
.errornote {
margin: 0 0 20px;
padding: 8px 12px;
font-size: 0.8125rem;
}
/* Calendar and clock */
.calendarbox, .clockbox {
position: fixed !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%);
margin: 0;
border: none;
overflow: visible;
}
.calendarbox:before, .clockbox:before {
content: '';
position: fixed;
top: 50%;
left: 50%;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
transform: translate(-50%, -50%);
}
.calendarbox > *, .clockbox > * {
position: relative;
z-index: 1;
}
.calendarbox > div:first-child {
z-index: 2;
}
.calendarbox .calendar, .clockbox h2 {
border-radius: 4px 4px 0 0;
overflow: hidden;
}
.calendarbox .calendar-cancel, .clockbox .calendar-cancel {
border-radius: 0 0 4px 4px;
overflow: hidden;
}
.calendar-shortcuts {
padding: 10px 0;
font-size: 0.75rem;
line-height: 0.75rem;
}
.calendar-shortcuts a {
margin: 0 4px;
}
.timelist a {
background: var(--body-bg);
padding: 4px;
}
.calendar-cancel {
padding: 8px 10px;
}
.clockbox h2 {
padding: 8px 15px;
}
.calendar caption {
padding: 10px;
}
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
z-index: 1;
top: 10px;
}
/* History */
table#change-history tbody th, table#change-history tbody td {
font-size: 0.8125rem;
word-break: break-word;
}
table#change-history tbody th {
width: auto;
}
/* Docs */
table.model tbody th, table.model tbody td {
font-size: 0.8125rem;
word-break: break-word;
}
}

View file

@ -0,0 +1,111 @@
/* TABLETS */
@media (max-width: 1024px) {
[dir="rtl"] .colMS {
margin-right: 0;
}
[dir="rtl"] #user-tools {
text-align: right;
}
[dir="rtl"] #changelist .actions label {
padding-left: 10px;
padding-right: 0;
}
[dir="rtl"] #changelist .actions select {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .change-list .filtered .results,
[dir="rtl"] .change-list .filtered .paginator,
[dir="rtl"] .filtered #toolbar,
[dir="rtl"] .filtered div.xfull,
[dir="rtl"] .filtered .actions,
[dir="rtl"] #changelist-filter {
margin-left: 0;
}
[dir="rtl"] .inline-group ul.tools a.add,
[dir="rtl"] .inline-group div.add-row a,
[dir="rtl"] .inline-group .tabular tr.add-row td a {
padding: 8px 26px 8px 10px;
background-position: calc(100% - 8px) 9px;
}
[dir="rtl"] .selector .selector-filter label {
margin-right: 0;
margin-left: 8px;
}
[dir="rtl"] .object-tools li {
float: right;
}
[dir="rtl"] .object-tools li + li {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .dashboard .module table td a {
padding-left: 0;
padding-right: 16px;
}
[dir="rtl"] .selector-add {
background-position: 0 -80px;
}
[dir="rtl"] .selector-remove {
background-position: 0 -120px;
}
[dir="rtl"] .active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -100px;
}
[dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -140px;
}
}
/* MOBILE */
@media (max-width: 767px) {
[dir="rtl"] .aligned .related-lookup,
[dir="rtl"] .aligned .datetimeshortcuts {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .aligned ul,
[dir="rtl"] form .aligned ul.errorlist {
margin-right: 0;
}
[dir="rtl"] #changelist-filter {
margin-left: 0;
margin-right: 0;
}
[dir="rtl"] .aligned .vCheckboxLabel {
padding: 1px 5px 0 0;
}
[dir="rtl"] .selector-remove {
background-position: 0 0;
}
[dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -20px;
}
[dir="rtl"] .selector-add {
background-position: 0 -40px;
}
[dir="rtl"] .active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -60px;
}
}

291
static/admin/css/rtl.css Normal file
View file

@ -0,0 +1,291 @@
/* GLOBAL */
th {
text-align: right;
}
.module h2, .module caption {
text-align: right;
}
.module ul, .module ol {
margin-left: 0;
margin-right: 1.5em;
}
.viewlink, .addlink, .changelink, .hidelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.deletelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.object-tools {
float: left;
}
thead th:first-child,
tfoot td:first-child {
border-left: none;
}
/* LAYOUT */
#user-tools {
right: auto;
left: 0;
text-align: left;
}
div.breadcrumbs {
text-align: right;
}
#content-main {
float: right;
}
#content-related {
float: left;
margin-left: -300px;
margin-right: auto;
}
.colMS {
margin-left: 300px;
margin-right: 0;
}
/* SORTABLE TABLES */
table thead th.sorted .sortoptions {
float: left;
}
thead th.sorted .text {
padding-right: 0;
padding-left: 42px;
}
/* dashboard styles */
.dashboard .module table td a {
padding-left: .6em;
padding-right: 16px;
}
/* changelists styles */
.change-list .filtered table {
border-left: none;
border-right: 0px none;
}
#changelist-filter {
border-left: none;
border-right: none;
margin-left: 0;
margin-right: 30px;
}
#changelist-filter li.selected {
border-left: none;
padding-left: 10px;
margin-left: 0;
border-right: 5px solid var(--hairline-color);
padding-right: 10px;
margin-right: -15px;
}
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
border-right: none;
border-left: none;
}
.paginator .end {
margin-left: 6px;
margin-right: 0;
}
.paginator input {
margin-left: 0;
margin-right: auto;
}
/* FORMS */
.aligned label {
padding: 0 0 3px 1em;
}
.submit-row a.deletelink {
margin-left: 0;
margin-right: auto;
}
.vDateField, .vTimeField {
margin-left: 2px;
}
.aligned .form-row input {
margin-left: 5px;
}
form .aligned ul {
margin-right: 163px;
padding-right: 10px;
margin-left: 0;
padding-left: 0;
}
form ul.inline li {
float: right;
padding-right: 0;
padding-left: 7px;
}
form .aligned p.help,
form .aligned div.help {
margin-left: 0;
margin-right: 160px;
padding-right: 10px;
}
form div.help ul,
form .aligned .checkbox-row + .help,
form .aligned p.date div.help.timezonewarning,
form .aligned p.datetime div.help.timezonewarning,
form .aligned p.time div.help.timezonewarning {
margin-right: 0;
padding-right: 0;
}
form .wide p.help,
form .wide ul.errorlist,
form .wide div.help {
padding-left: 0;
padding-right: 50px;
}
.submit-row {
text-align: right;
}
fieldset .fieldBox {
margin-left: 20px;
margin-right: 0;
}
.errorlist li {
background-position: 100% 12px;
padding: 0;
}
.errornote {
background-position: 100% 12px;
padding: 10px 12px;
}
/* WIDGETS */
.calendarnav-previous {
top: 0;
left: auto;
right: 10px;
background: url(../img/calendar-icons.svg) 0 -15px no-repeat;
}
.calendarnav-next {
top: 0;
right: auto;
left: 10px;
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
}
.calendar caption, .calendarbox h2 {
text-align: center;
}
.selector {
float: right;
}
.selector .selector-filter {
text-align: right;
}
.selector-add {
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -80px;
}
.selector-remove {
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -112px;
}
a.selector-chooseall {
background: url(../img/selector-icons.svg) right -128px no-repeat;
}
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
background-position: 100% -144px;
}
a.selector-clearall {
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
}
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
background-position: 0 -176px;
}
.inline-deletelink {
float: left;
}
form .form-row p.datetime {
overflow: hidden;
}
.related-widget-wrapper {
float: right;
}
/* MISC */
.inline-related h2, .inline-group h2 {
text-align: right
}
.inline-related h3 span.delete {
padding-right: 20px;
padding-left: inherit;
left: 10px;
right: inherit;
float:left;
}
.inline-related h3 span.delete label {
margin-left: inherit;
margin-right: 2px;
}
.inline-group .tabular td.original p {
right: 0;
}
.selector .selector-chooser {
margin: 0;
}

View file

@ -0,0 +1,19 @@
/* Hide warnings fields if usable password is selected */
form:has(#id_usable_password input[value="true"]:checked) .messagelist {
display: none;
}
/* Hide password fields if unusable password is selected */
form:has(#id_usable_password input[value="false"]:checked) .field-password1,
form:has(#id_usable_password input[value="false"]:checked) .field-password2 {
display: none;
}
/* Select appropriate submit button */
form:has(#id_usable_password input[value="true"]:checked) input[type="submit"].unset-password {
display: none;
}
form:has(#id_usable_password input[value="false"]:checked) input[type="submit"].set-password {
display: none;
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,481 @@
.select2-container {
box-sizing: border-box;
display: inline-block;
margin: 0;
position: relative;
vertical-align: middle; }
.select2-container .select2-selection--single {
box-sizing: border-box;
cursor: pointer;
display: block;
height: 28px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--single .select2-selection__rendered {
display: block;
padding-left: 8px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-selection--single .select2-selection__clear {
position: relative; }
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 8px;
padding-left: 20px; }
.select2-container .select2-selection--multiple {
box-sizing: border-box;
cursor: pointer;
display: block;
min-height: 32px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--multiple .select2-selection__rendered {
display: inline-block;
overflow: hidden;
padding-left: 8px;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-search--inline {
float: left; }
.select2-container .select2-search--inline .select2-search__field {
box-sizing: border-box;
border: none;
font-size: 100%;
margin-top: 5px;
padding: 0; }
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-dropdown {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
box-sizing: border-box;
display: block;
position: absolute;
left: -100000px;
width: 100%;
z-index: 1051; }
.select2-results {
display: block; }
.select2-results__options {
list-style: none;
margin: 0;
padding: 0; }
.select2-results__option {
padding: 6px;
user-select: none;
-webkit-user-select: none; }
.select2-results__option[aria-selected] {
cursor: pointer; }
.select2-container--open .select2-dropdown {
left: 0; }
.select2-container--open .select2-dropdown--above {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--open .select2-dropdown--below {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-search--dropdown {
display: block;
padding: 4px; }
.select2-search--dropdown .select2-search__field {
padding: 4px;
width: 100%;
box-sizing: border-box; }
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-search--dropdown.select2-search--hide {
display: none; }
.select2-close-mask {
border: 0;
margin: 0;
padding: 0;
display: block;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 99;
background-color: #fff;
filter: alpha(opacity=0); }
.select2-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important; }
.select2-container--default .select2-selection--single {
background-color: #fff;
border: 1px solid #aaa;
border-radius: 4px; }
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--default .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold; }
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px; }
.select2-container--default .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto; }
.select2-container--default.select2-container--disabled .select2-selection--single {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none; }
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--default .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 5px;
width: 100%; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
list-style: none; }
.select2-container--default .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-top: 5px;
margin-right: 10px;
padding: 1px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--default.select2-container--focus .select2-selection--multiple {
border: solid black 1px;
outline: 0; }
.select2-container--default.select2-container--disabled .select2-selection--multiple {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
display: none; }
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--default .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa; }
.select2-container--default .select2-search--inline .select2-search__field {
background: transparent;
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield; }
.select2-container--default .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--default .select2-results__option[role=group] {
padding: 0; }
.select2-container--default .select2-results__option[aria-disabled=true] {
color: #999; }
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #ddd; }
.select2-container--default .select2-results__option .select2-results__option {
padding-left: 1em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em; }
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #5897fb;
color: white; }
.select2-container--default .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic .select2-selection--single {
background-color: #f7f7f7;
border: 1px solid #aaa;
border-radius: 4px;
outline: 0;
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic .select2-selection--single:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--classic .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px; }
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--classic .select2-selection--single .select2-selection__arrow {
background-color: #ddd;
border: none;
border-left: 1px solid #aaa;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
border: none;
border-right: 1px solid #aaa;
border-radius: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
left: 1px;
right: auto; }
.select2-container--classic.select2-container--open .select2-selection--single {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
background: transparent;
border: none; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
.select2-container--classic .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text;
outline: 0; }
.select2-container--classic .select2-selection--multiple:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
list-style: none;
margin: 0;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
display: none; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
color: #888;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #555; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
float: right;
margin-left: 5px;
margin-right: auto; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--classic.select2-container--open .select2-selection--multiple {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--classic .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa;
outline: 0; }
.select2-container--classic .select2-search--inline .select2-search__field {
outline: 0;
box-shadow: none; }
.select2-container--classic .select2-dropdown {
background-color: white;
border: 1px solid transparent; }
.select2-container--classic .select2-dropdown--above {
border-bottom: none; }
.select2-container--classic .select2-dropdown--below {
border-top: none; }
.select2-container--classic .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--classic .select2-results__option[role=group] {
padding: 0; }
.select2-container--classic .select2-results__option[aria-disabled=true] {
color: grey; }
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
background-color: #3875d7;
color: white; }
.select2-container--classic .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic.select2-container--open .select2-dropdown {
border-color: #5897fb; }

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,593 @@
/* SELECTOR (FILTER INTERFACE) */
.selector {
display: flex;
flex-grow: 1;
gap: 0 10px;
}
.selector select {
height: 17.2em;
flex: 1 0 auto;
overflow: scroll;
width: 100%;
}
.selector-available, .selector-chosen {
text-align: center;
display: flex;
flex-direction: column;
flex: 1 1;
}
.selector-available h2, .selector-chosen h2 {
border: 1px solid var(--border-color);
border-radius: 4px 4px 0 0;
}
.selector-chosen .list-footer-display {
border: 1px solid var(--border-color);
border-top: none;
border-radius: 0 0 4px 4px;
margin: 0 0 10px;
padding: 8px;
text-align: center;
background: var(--primary);
color: var(--header-link-color);
cursor: pointer;
}
.selector-chosen .list-footer-display__clear {
color: var(--breadcrumbs-fg);
}
.selector-chosen h2 {
background: var(--secondary);
color: var(--header-link-color);
}
.selector .selector-available h2 {
background: var(--darkened-bg);
color: var(--body-quiet-color);
}
.selector .selector-filter {
border: 1px solid var(--border-color);
border-width: 0 1px;
padding: 8px;
color: var(--body-quiet-color);
font-size: 0.625rem;
margin: 0;
text-align: left;
display: flex;
}
.selector .selector-filter label,
.inline-group .aligned .selector .selector-filter label {
float: left;
margin: 7px 0 0;
width: 18px;
height: 18px;
padding: 0;
overflow: hidden;
line-height: 1;
min-width: auto;
}
.selector-filter input {
flex-grow: 1;
}
.selector .selector-available input,
.selector .selector-chosen input {
margin-left: 8px;
}
.selector ul.selector-chooser {
align-self: center;
width: 22px;
background-color: var(--selected-bg);
border-radius: 10px;
margin: 0;
padding: 0;
transform: translateY(-17px);
}
.selector-chooser li {
margin: 0;
padding: 3px;
list-style-type: none;
}
.selector select {
padding: 0 10px;
margin: 0 0 10px;
border-radius: 0 0 4px 4px;
}
.selector .selector-chosen--with-filtered select {
margin: 0;
border-radius: 0;
height: 14em;
}
.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display {
display: none;
}
.selector-add, .selector-remove {
width: 16px;
height: 16px;
display: block;
text-indent: -3000px;
overflow: hidden;
cursor: default;
opacity: 0.55;
}
.active.selector-add, .active.selector-remove {
opacity: 1;
}
.active.selector-add:hover, .active.selector-remove:hover {
cursor: pointer;
}
.selector-add {
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -112px;
}
.selector-remove {
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
display: inline-block;
height: 16px;
text-align: left;
margin: 0 auto;
overflow: hidden;
font-weight: bold;
line-height: 16px;
color: var(--body-quiet-color);
text-decoration: none;
opacity: 0.55;
}
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
color: var(--link-fg);
}
a.active.selector-chooseall, a.active.selector-clearall {
opacity: 1;
}
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
cursor: pointer;
}
a.selector-chooseall {
padding: 0 18px 0 0;
background: url(../img/selector-icons.svg) right -160px no-repeat;
cursor: default;
}
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
background-position: 100% -176px;
}
a.selector-clearall {
padding: 0 0 0 18px;
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
cursor: default;
}
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
background-position: 0 -144px;
}
/* STACKED SELECTORS */
.stacked {
float: left;
width: 490px;
display: block;
}
.stacked select {
width: 480px;
height: 10.1em;
}
.stacked .selector-available, .stacked .selector-chosen {
width: 480px;
}
.stacked .selector-available {
margin-bottom: 0;
}
.stacked .selector-available input {
width: 422px;
}
.stacked ul.selector-chooser {
height: 22px;
width: 50px;
margin: 0 0 10px 40%;
background-color: #eee;
border-radius: 10px;
transform: none;
}
.stacked .selector-chooser li {
float: left;
padding: 3px 3px 3px 5px;
}
.stacked .selector-chooseall, .stacked .selector-clearall {
display: none;
}
.stacked .selector-add {
background: url(../img/selector-icons.svg) 0 -32px no-repeat;
cursor: default;
}
.stacked .active.selector-add {
background-position: 0 -32px;
cursor: pointer;
}
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
background-position: 0 -48px;
cursor: pointer;
}
.stacked .selector-remove {
background: url(../img/selector-icons.svg) 0 0 no-repeat;
cursor: default;
}
.stacked .active.selector-remove {
background-position: 0 0px;
cursor: pointer;
}
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
background-position: 0 -16px;
cursor: pointer;
}
.selector .help-icon {
background: url(../img/icon-unknown.svg) 0 0 no-repeat;
display: inline-block;
vertical-align: middle;
margin: -2px 0 0 2px;
width: 13px;
height: 13px;
}
.selector .selector-chosen .help-icon {
background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
}
.selector .search-label-icon {
background: url(../img/search.svg) 0 0 no-repeat;
display: inline-block;
height: 1.125rem;
width: 1.125rem;
}
/* DATE AND TIME */
p.datetime {
line-height: 20px;
margin: 0;
padding: 0;
color: var(--body-quiet-color);
font-weight: bold;
}
.datetime span {
white-space: nowrap;
font-weight: normal;
font-size: 0.6875rem;
color: var(--body-quiet-color);
}
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
margin-left: 5px;
margin-bottom: 4px;
}
table p.datetime {
font-size: 0.6875rem;
margin-left: 0;
padding-left: 0;
}
.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
position: relative;
display: inline-block;
vertical-align: middle;
height: 16px;
width: 16px;
overflow: hidden;
}
.datetimeshortcuts .clock-icon {
background: url(../img/icon-clock.svg) 0 0 no-repeat;
}
.datetimeshortcuts a:focus .clock-icon,
.datetimeshortcuts a:hover .clock-icon {
background-position: 0 -16px;
}
.datetimeshortcuts .date-icon {
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
top: -1px;
}
.datetimeshortcuts a:focus .date-icon,
.datetimeshortcuts a:hover .date-icon {
background-position: 0 -16px;
}
.timezonewarning {
font-size: 0.6875rem;
color: var(--body-quiet-color);
}
/* URL */
p.url {
line-height: 20px;
margin: 0;
padding: 0;
color: var(--body-quiet-color);
font-size: 0.6875rem;
font-weight: bold;
}
.url a {
font-weight: normal;
}
/* FILE UPLOADS */
p.file-upload {
line-height: 20px;
margin: 0;
padding: 0;
color: var(--body-quiet-color);
font-size: 0.6875rem;
font-weight: bold;
}
.file-upload a {
font-weight: normal;
}
.file-upload .deletelink {
margin-left: 5px;
}
span.clearable-file-input label {
color: var(--body-fg);
font-size: 0.6875rem;
display: inline;
float: none;
}
/* CALENDARS & CLOCKS */
.calendarbox, .clockbox {
margin: 5px auto;
font-size: 0.75rem;
width: 19em;
text-align: center;
background: var(--body-bg);
color: var(--body-fg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
overflow: hidden;
position: relative;
}
.clockbox {
width: auto;
}
.calendar {
margin: 0;
padding: 0;
}
.calendar table {
margin: 0;
padding: 0;
border-collapse: collapse;
background: white;
width: 100%;
}
.calendar caption, .calendarbox h2 {
margin: 0;
text-align: center;
border-top: none;
font-weight: 700;
font-size: 0.75rem;
color: #333;
background: var(--accent);
}
.calendar th {
padding: 8px 5px;
background: var(--darkened-bg);
border-bottom: 1px solid var(--border-color);
font-weight: 400;
font-size: 0.75rem;
text-align: center;
color: var(--body-quiet-color);
}
.calendar td {
font-weight: 400;
font-size: 0.75rem;
text-align: center;
padding: 0;
border-top: 1px solid var(--hairline-color);
border-bottom: none;
}
.calendar td.selected a {
background: var(--secondary);
color: var(--button-fg);
}
.calendar td.nonday {
background: var(--darkened-bg);
}
.calendar td.today a {
font-weight: 700;
}
.calendar td a, .timelist a {
display: block;
font-weight: 400;
padding: 6px;
text-decoration: none;
color: var(--body-quiet-color);
}
.calendar td a:focus, .timelist a:focus,
.calendar td a:hover, .timelist a:hover {
background: var(--primary);
color: white;
}
.calendar td a:active, .timelist a:active {
background: var(--header-bg);
color: white;
}
.calendarnav {
font-size: 0.625rem;
text-align: center;
color: #ccc;
margin: 0;
padding: 1px 3px;
}
.calendarnav a:link, #calendarnav a:visited,
#calendarnav a:focus, #calendarnav a:hover {
color: var(--body-quiet-color);
}
.calendar-shortcuts {
background: var(--body-bg);
color: var(--body-quiet-color);
font-size: 0.6875rem;
line-height: 0.6875rem;
border-top: 1px solid var(--hairline-color);
padding: 8px 0;
}
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
display: block;
position: absolute;
top: 8px;
width: 15px;
height: 15px;
text-indent: -9999px;
padding: 0;
}
.calendarnav-previous {
left: 10px;
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
}
.calendarnav-next {
right: 10px;
background: url(../img/calendar-icons.svg) 0 -15px no-repeat;
}
.calendar-cancel {
margin: 0;
padding: 4px 0;
font-size: 0.75rem;
background: var(--close-button-bg);
border-top: 1px solid var(--border-color);
color: var(--button-fg);
}
.calendar-cancel:focus, .calendar-cancel:hover {
background: var(--close-button-hover-bg);
}
.calendar-cancel a {
color: var(--button-fg);
display: block;
}
ul.timelist, .timelist li {
list-style-type: none;
margin: 0;
padding: 0;
}
.timelist a {
padding: 2px;
}
/* EDIT INLINE */
.inline-deletelink {
float: right;
text-indent: -9999px;
background: url(../img/inline-delete.svg) 0 0 no-repeat;
width: 16px;
height: 16px;
border: 0px none;
}
.inline-deletelink:focus, .inline-deletelink:hover {
cursor: pointer;
}
/* RELATED WIDGET WRAPPER */
.related-widget-wrapper {
display: flex;
gap: 0 10px;
flex-grow: 1;
flex-wrap: wrap;
margin-bottom: 5px;
}
.related-widget-wrapper-link {
opacity: .6;
filter: grayscale(1);
}
.related-widget-wrapper-link:link {
opacity: 1;
filter: grayscale(0);
}
/* GIS MAPS */
.dj_map {
width: 600px;
height: 400px;
}

20
static/admin/img/LICENSE Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Code Charm Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,7 @@
All icons are taken from Font Awesome (https://fontawesome.com/) project.
The Font Awesome font is licensed under the SIL OFL 1.1:
- https://scripts.sil.org/OFL
SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG
Font-Awesome-SVG-PNG is licensed under the MIT license (see file license
in current folder).

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="15"
height="30"
viewBox="0 0 1792 3584"
version="1.1"
id="svg5"
sodipodi:docname="calendar-icons.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview5"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="13.3"
inkscape:cx="15.526316"
inkscape:cy="20.977444"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<defs
id="defs2">
<g
id="previous">
<path
d="m 1037,1395 102,-102 q 19,-19 19,-45 0,-26 -19,-45 L 832,896 1139,589 q 19,-19 19,-45 0,-26 -19,-45 L 1037,397 q -19,-19 -45,-19 -26,0 -45,19 L 493,851 q -19,19 -19,45 0,26 19,45 l 454,454 q 19,19 45,19 26,0 45,-19 z m 627,-499 q 0,209 -103,385.5 Q 1458,1458 1281.5,1561 1105,1664 896,1664 687,1664 510.5,1561 334,1458 231,1281.5 128,1105 128,896 128,687 231,510.5 334,334 510.5,231 687,128 896,128 1105,128 1281.5,231 1458,334 1561,510.5 1664,687 1664,896 Z"
id="path1" />
</g>
<g
id="next">
<path
d="m 845,1395 454,-454 q 19,-19 19,-45 0,-26 -19,-45 L 845,397 q -19,-19 -45,-19 -26,0 -45,19 L 653,499 q -19,19 -19,45 0,26 19,45 l 307,307 -307,307 q -19,19 -19,45 0,26 19,45 l 102,102 q 19,19 45,19 26,0 45,-19 z m 819,-499 q 0,209 -103,385.5 Q 1458,1458 1281.5,1561 1105,1664 896,1664 687,1664 510.5,1561 334,1458 231,1281.5 128,1105 128,896 128,687 231,510.5 334,334 510.5,231 687,128 896,128 1105,128 1281.5,231 1458,334 1561,510.5 1664,687 1664,896 Z"
id="path2" />
</g>
</defs>
<use
xlink:href="#next"
x="0"
y="5376"
fill="#000000"
id="use5"
transform="translate(0,-3584)" />
<use
xlink:href="#previous"
x="0"
y="0"
fill="#333333"
id="use2"
style="fill:#000000;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1 @@
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#EBECE6" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9C9C9" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#F1C02A" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9A741" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#5fa225" d="M1600 796v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#efb80b" d="M1024 1375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
</svg>

After

Width:  |  Height:  |  Size: 504 B

View file

@ -0,0 +1,9 @@
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="icon">
<path d="M192 1664h288v-288h-288v288zm352 0h320v-288h-320v288zm-352-352h288v-320h-288v320zm352 0h320v-320h-320v320zm-352-384h288v-288h-288v288zm736 736h320v-288h-320v288zm-384-736h320v-288h-320v288zm768 736h288v-288h-288v288zm-384-352h320v-320h-320v320zm-352-864v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm736 864h288v-320h-288v320zm-384-384h320v-288h-320v288zm384 0h288v-288h-288v288zm32-480v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm384-64v1280q0 52-38 90t-90 38h-1408q-52 0-90-38t-38-90v-1280q0-52 38-90t90-38h128v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h384v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h128q52 0 90 38t38 90z"/>
</g>
</defs>
<use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
<use xlink:href="#icon" x="0" y="1792" fill="#003366" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#b48c08" d="M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z"/>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View file

@ -0,0 +1,9 @@
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="icon">
<path d="M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
</defs>
<use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
<use xlink:href="#icon" x="0" y="1792" fill="#003366" />
</svg>

After

Width:  |  Height:  |  Size: 677 B

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#dd4646" d="M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#2b70bf" d="m555 1335 78-141q-87-63-136-159t-49-203q0-121 61-225-229 117-381 353 167 258 427 375zm389-759q0-20-14-34t-34-14q-125 0-214.5 89.5T592 832q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm363-191q0 7-1 9-105 188-315 566t-316 567l-49 89q-10 16-28 16-12 0-134-70-16-10-16-28 0-12 44-87-143-65-263.5-173T20 1029Q0 998 0 960t20-69q153-235 380-371t496-136q89 0 180 17l54-97q10-16 28-16 5 0 18 6t31 15.5 33 18.5 31.5 18.5T1291 358q16 10 16 27zm37 447q0 139-79 253.5T1056 1250l280-502q8 45 8 84zm448 128q0 35-20 69-39 64-109 145-150 172-347.5 267T896 1536l74-132q212-18 392.5-137T1664 960q-115-179-282-294l63-112q95 64 182.5 153T1772 891q20 34 20 69z"/>
</svg>

After

Width:  |  Height:  |  Size: 784 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#dd4646" d="M1277 1122q0-26-19-45l-181-181 181-181q19-19 19-45 0-27-19-46l-90-90q-19-19-46-19-26 0-45 19l-181 181-181-181q-19-19-45-19-27 0-46 19l-90 90q-19 19-19 46 0 26 19 45l181 181-181 181q-19 19-19 45 0 27 19 46l90 90q19 19 46 19 26 0 45-19l181-181 181 181q19 19 45 19 27 0 46-19l90-90q19-19 19-46zm387-226q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 560 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M1024 1376v-192q0-14-9-23t-23-9h-192q-14 0-23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23-9t9-23zm256-672q0-88-55.5-163t-138.5-116-170-41q-243 0-371 213-15 24 8 42l132 100q7 6 19 6 16 0 25-12 53-68 86-92 34-24 86-24 48 0 85.5 26t37.5 59q0 38-20 61t-68 45q-63 28-115.5 86.5t-52.5 125.5v36q0 14 9 23t23 9h192q14 0 23-9t9-23q0-19 21.5-49.5t54.5-49.5q32-18 49-28.5t46-35 44.5-48 28-60.5 12.5-81zm384 192q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 655 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#666666" d="M1024 1376v-192q0-14-9-23t-23-9h-192q-14 0-23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23-9t9-23zm256-672q0-88-55.5-163t-138.5-116-170-41q-243 0-371 213-15 24 8 42l132 100q7 6 19 6 16 0 25-12 53-68 86-92 34-24 86-24 48 0 85.5 26t37.5 59q0 38-20 61t-68 45q-63 28-115.5 86.5t-52.5 125.5v36q0 14 9 23t23 9h192q14 0 23-9t9-23q0-19 21.5-49.5t54.5-49.5q32-18 49-28.5t46-35 44.5-48 28-60.5 12.5-81zm384 192q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 655 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#2b70bf" d="M1664 960q-152-236-381-353 61 104 61 225 0 185-131.5 316.5t-316.5 131.5-316.5-131.5-131.5-316.5q0-121 61-225-229 117-381 353 133 205 333.5 326.5t434.5 121.5 434.5-121.5 333.5-326.5zm-720-384q0-20-14-34t-34-14q-125 0-214.5 89.5t-89.5 214.5q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm848 384q0 34-20 69-140 230-376.5 368.5t-499.5 138.5-499.5-139-376.5-368q-20-35-20-69t20-69q140-229 376.5-368t499.5-139 499.5 139 376.5 368q20 35 20 69z"/>
</svg>

After

Width:  |  Height:  |  Size: 581 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#70bf2b" d="M1412 734q0-28-18-46l-91-90q-19-19-45-19t-45 19l-408 407-226-226q-19-19-45-19t-45 19l-91 90q-18 18-18 46 0 27 18 45l362 362q19 19 45 19 27 0 46-19l543-543q18-18 18-45zm252 162q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Some files were not shown because too many files have changed in this diff Show more