fix the value too long issue in django plan
This commit is contained in:
parent
4b7bf44923
commit
5884a0cb8d
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.7 on 2025-07-01 10:33
|
||||
# Generated by Django 5.2.4 on 2025-07-09 13:00
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.7 on 2025-07-01 10:33
|
||||
# Generated by Django 5.2.4 on 2025-07-09 13:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -2035,7 +2035,17 @@ class CSVUploadForm(forms.Form):
|
||||
)
|
||||
year = forms.IntegerField(
|
||||
label=_("Year"),
|
||||
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
||||
widget=forms.NumberInput(attrs=
|
||||
{
|
||||
"class": "form-control",
|
||||
"hx-get": "",
|
||||
"hx-target": "#serie",
|
||||
"hx-select": "#serie",
|
||||
"hx-include": "#model",
|
||||
"hx-trigger": "input delay:500ms",
|
||||
"hx-swap": "outerHTML",
|
||||
}
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
exterior = forms.ModelChoiceField(
|
||||
|
||||
@ -96,10 +96,23 @@ class InjectDealerMiddleware:
|
||||
if request.user.is_authenticated:
|
||||
request.is_dealer = False
|
||||
request.is_staff = False
|
||||
request.is_manager = False
|
||||
request.is_accountant = False
|
||||
request.is_sales = False
|
||||
request.is_inventory = False
|
||||
if hasattr(request.user, "dealer"):
|
||||
request.is_dealer = True
|
||||
elif hasattr(request.user, "staffmember"):
|
||||
request.is_staff = True
|
||||
staff = getattr(request.user.staffmember, "staff")
|
||||
if "Accountant" in staff.groups.values_list("name", flat=True):
|
||||
request.is_accountant = True
|
||||
if "Manager" in staff.groups.values_list("name", flat=True):
|
||||
request.is_manager = True
|
||||
if "Sales" in staff.groups.values_list("name", flat=True):
|
||||
request.is_sales = True
|
||||
if "Inventory" in staff.groups.values_list("name", flat=True):
|
||||
request.is_inventory = True
|
||||
except Exception:
|
||||
pass
|
||||
response = self.get_response(request)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# Generated by Django 5.1.7 on 2025-07-01 10:33
|
||||
# Generated by Django 5.2.4 on 2025-07-09 13:00
|
||||
|
||||
import datetime
|
||||
import django.core.serializers.json
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
@ -21,7 +22,7 @@ class Migration(migrations.Migration):
|
||||
('appointment', '0001_initial'),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
|
||||
('django_ledger', '0023_merge_20250708_1825'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
@ -84,20 +85,6 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Payment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
|
||||
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
|
||||
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
|
||||
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'payment',
|
||||
'verbose_name_plural': 'payments',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VatRate',
|
||||
fields=[
|
||||
@ -600,6 +587,21 @@ class Migration(migrations.Migration):
|
||||
name='organization',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.organization'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Payment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
|
||||
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
|
||||
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
|
||||
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
|
||||
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='django_ledger.invoicemodel', verbose_name='invoice')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'payment',
|
||||
'verbose_name_plural': 'payments',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PoItemsUploaded',
|
||||
fields=[
|
||||
@ -677,16 +679,19 @@ class Migration(migrations.Migration):
|
||||
name='Schedule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('purpose', models.CharField(choices=[('product_demo', 'Product Demo'), ('follow_up_call', 'Follow-Up Call'), ('contract_discussion', 'Contract Discussion'), ('sales_meeting', 'Sales Meeting'), ('support_call', 'Support Call'), ('other', 'Other')], max_length=200)),
|
||||
('scheduled_at', models.DateTimeField()),
|
||||
('scheduled_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email')], default='Call', max_length=200)),
|
||||
('completed', models.BooleanField(default=False, verbose_name='Completed')),
|
||||
('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
|
||||
('notes', models.TextField(blank=True, null=True)),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='django_ledger.customermodel')),
|
||||
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='inventory.lead')),
|
||||
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer')),
|
||||
('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
@ -836,6 +841,24 @@ class Migration(migrations.Migration):
|
||||
'unique_together': {('dealer', 'car_make')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExtraInfo',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('related_object_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_info_primary', to='contenttypes.contenttype')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_extra_info', to=settings.AUTH_USER_MODEL)),
|
||||
('related_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='extra_info_secondary', to='contenttypes.contenttype')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Extra Info',
|
||||
'indexes': [models.Index(fields=['content_type', 'object_id'], name='inventory_e_content_2ecbed_idx'), models.Index(fields=['related_content_type', 'related_object_id'], name='inventory_e_related_8680bb_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CarColors',
|
||||
fields=[
|
||||
|
||||
@ -1188,10 +1188,7 @@ class Staff(models.Model, LocalizedNameMixin):
|
||||
|
||||
@property
|
||||
def groups(self):
|
||||
return [x.customgroup for x in self.user.groups.all()]
|
||||
@property
|
||||
def groups(self):
|
||||
return [x.customgroup for x in self.user.groups.all()]
|
||||
return CustomGroup.objects.filter(pk__in=[x.customgroup.pk for x in self.user.groups.all()])
|
||||
|
||||
def clear_groups(self):
|
||||
self.remove_superuser_permission()
|
||||
@ -2568,16 +2565,8 @@ class CustomGroup(models.Model):
|
||||
pass
|
||||
|
||||
def set_default_permissions(self):
|
||||
Permission.objects.get_or_create(name="Can approve estimate",codename="can_approve_estimatemodel",content_type=ContentType.objects.get_for_model(EstimateModel))
|
||||
Permission.objects.get_or_create(name="Can approve bill",codename="can_approve_billmodel",content_type=ContentType.objects.get_for_model(BillModel))
|
||||
|
||||
Permission.objects.get_or_create(name="Can view inventory",codename="can_view_inventory",content_type=ContentType.objects.get_for_model(Car))
|
||||
Permission.objects.get_or_create(name="Can view sales",codename="can_view_sales",content_type=ContentType.objects.get_for_model(EstimateModel))
|
||||
Permission.objects.get_or_create(name="Can view crm",codename="can_view_crm",content_type=ContentType.objects.get_for_model(Lead))
|
||||
Permission.objects.get_or_create(name="Can view financials",codename="can_view_financials",content_type=ContentType.objects.get_for_model(AccountModel))
|
||||
Permission.objects.get_or_create(name="Can view reports",codename="can_view_reports",content_type=ContentType.objects.get_for_model(LedgerModel))
|
||||
|
||||
self.clear_permissions()
|
||||
|
||||
######################################
|
||||
######################################
|
||||
#MANAGER
|
||||
|
||||
@ -171,7 +171,7 @@ def create_ledger_entity(sender, instance, created, **kwargs):
|
||||
entity.create_uom(name=u[1], unit_abbr=u[0])
|
||||
|
||||
# Create COA accounts, background task
|
||||
async_task(create_coa_accounts,instance.pk)
|
||||
async_task(create_coa_accounts,instance)
|
||||
|
||||
# create_settings(instance.pk)
|
||||
# create_accounts_for_make(instance.pk)
|
||||
@ -192,20 +192,20 @@ def create_dealer_groups(sender, instance, created, **kwargs):
|
||||
:param kwargs: Additional keyword arguments passed by the signal.
|
||||
:type kwargs: dict
|
||||
"""
|
||||
group_names = ["Inventory", "Accountant", "Sales","Manager"]
|
||||
if created:
|
||||
# async_task("inventory.tasks.create_groups",instance.slug)
|
||||
def create_groups():
|
||||
for group_name in ["Inventory", "Accountant", "Sales","Manager"]:
|
||||
group= Group.objects.create(
|
||||
name=f"{instance.slug}_{group_name}"
|
||||
)
|
||||
group_manager = models.CustomGroup.objects.create(
|
||||
name=group_name, dealer=instance, group=group
|
||||
)
|
||||
group_manager.set_default_permissions()
|
||||
instance.user.groups.add(group)
|
||||
|
||||
def create_groups():
|
||||
for group_name in group_names:
|
||||
group, created = Group.objects.get_or_create(
|
||||
name=f"{instance.slug}_{group_name}"
|
||||
)
|
||||
group_manager, created = models.CustomGroup.objects.get_or_create(
|
||||
name=group_name, dealer=instance, group=group
|
||||
)
|
||||
group_manager.set_default_permissions()
|
||||
instance.user.groups.add(group)
|
||||
|
||||
transaction.on_commit(create_groups)
|
||||
transaction.on_commit(create_groups)
|
||||
|
||||
|
||||
# Create Vendor
|
||||
@ -739,24 +739,24 @@ def create_dealer_settings(sender, instance, created, **kwargs):
|
||||
# VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.Dealer)
|
||||
def create_make_ledger_accounts(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Signal receiver that creates ledger accounts for car makes associated with a dealer when a new dealer instance
|
||||
is created. This function listens to the `post_save` signal of the `Dealer` model and automatically generates
|
||||
new ledger accounts for all car makes, associating them with the given dealer's entity.
|
||||
# @receiver(post_save, sender=models.Dealer)
|
||||
# def create_make_ledger_accounts(sender, instance, created, **kwargs):
|
||||
# """
|
||||
# Signal receiver that creates ledger accounts for car makes associated with a dealer when a new dealer instance
|
||||
# is created. This function listens to the `post_save` signal of the `Dealer` model and automatically generates
|
||||
# new ledger accounts for all car makes, associating them with the given dealer's entity.
|
||||
|
||||
:param sender: The model class (`Dealer`) that triggered the signal.
|
||||
:type sender: Type[models.Dealer]
|
||||
:param instance: The instance of the `Dealer` model that triggered the signal.
|
||||
:param created: A boolean indicating whether a new `Dealer` instance was created.
|
||||
:type created: bool
|
||||
:param kwargs: Additional keyword arguments passed by the signal.
|
||||
:return: None
|
||||
"""
|
||||
if created:
|
||||
entity = instance.entity
|
||||
coa = entity.get_default_coa()
|
||||
# :param sender: The model class (`Dealer`) that triggered the signal.
|
||||
# :type sender: Type[models.Dealer]
|
||||
# :param instance: The instance of the `Dealer` model that triggered the signal.
|
||||
# :param created: A boolean indicating whether a new `Dealer` instance was created.
|
||||
# :type created: bool
|
||||
# :param kwargs: Additional keyword arguments passed by the signal.
|
||||
# :return: None
|
||||
# """
|
||||
# if created:
|
||||
# entity = instance.entity
|
||||
# coa = entity.get_default_coa()
|
||||
|
||||
# for make in models.CarMake.objects.all():
|
||||
# last_account = entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).order_by('-created').first()
|
||||
|
||||
@ -33,9 +33,8 @@ def create_settings(pk):
|
||||
)
|
||||
|
||||
|
||||
def create_coa_accounts(pk):
|
||||
def create_coa_accounts(instance):
|
||||
with transaction.atomic():
|
||||
instance = Dealer.objects.select_for_update().get(pk=pk)
|
||||
entity = instance.entity
|
||||
coa = entity.get_default_coa()
|
||||
|
||||
@ -1483,3 +1482,21 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
|
||||
address=address,
|
||||
)
|
||||
|
||||
|
||||
# def create_groups(dealer_slug):
|
||||
# from inventory.models import CustomGroup
|
||||
# instance = Dealer.objects.get(slug=dealer_slug)
|
||||
# def run():
|
||||
# for group_name in ["Inventory", "Accountant", "Sales", "Manager"]:
|
||||
# group, created = Group.objects.get_or_create(
|
||||
# name=f"{instance.slug}_{group_name}"
|
||||
# )
|
||||
# group_manager, created = CustomGroup.objects.get_or_create(
|
||||
# name=group_name, dealer=instance, group=group
|
||||
# )
|
||||
# if created:
|
||||
# group_manager.set_default_permissions()
|
||||
# instance.user.groups.add(group)
|
||||
|
||||
# transaction.on_commit(run)
|
||||
|
||||
|
||||
@ -473,6 +473,15 @@ def po_item_formset_table(context, po_model, itemtxs_formset,user):
|
||||
|
||||
@register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True)
|
||||
def bill_item_formset_table(context, item_formset):
|
||||
for item in item_formset:
|
||||
if item:
|
||||
item.initial['quantity'] = item.instance.po_quantity
|
||||
item.initial['unit_cost'] = item.instance.po_unit_cost
|
||||
# print(item.instance.po_quantity)
|
||||
# print(item.instance.po_unit_cost)
|
||||
# print(item.instance.po_total_amount)
|
||||
# if item.po_quantity:
|
||||
# item.quantity = item.po_quantity
|
||||
return {
|
||||
"dealer_slug": context["view"].kwargs["dealer_slug"],
|
||||
"entity_slug": context["view"].kwargs["entity_slug"],
|
||||
|
||||
@ -281,196 +281,196 @@ class AuthenticationTest(TestCase):
|
||||
) # Assuming the view returns an error for missing fields
|
||||
|
||||
|
||||
class CarFinanceCalculatorTests(TestCase):
|
||||
"""
|
||||
Unit tests for the CarFinanceCalculator class.
|
||||
# class CarFinanceCalculatorTests(TestCase):
|
||||
# """
|
||||
# Unit tests for the CarFinanceCalculator class.
|
||||
|
||||
This class contains various test cases to validate the correct functionality
|
||||
of the CarFinanceCalculator. It includes tests for VAT rate retrieval,
|
||||
item transactions, car data extraction, calculation of totals, fetching
|
||||
additional services, and finance-related data processing.
|
||||
# This class contains various test cases to validate the correct functionality
|
||||
# of the CarFinanceCalculator. It includes tests for VAT rate retrieval,
|
||||
# item transactions, car data extraction, calculation of totals, fetching
|
||||
# additional services, and finance-related data processing.
|
||||
|
||||
:ivar mock_model: Mocked model used to simulate interactions with the
|
||||
CarFinanceCalculator instance.
|
||||
:type mock_model: unittest.mock.MagicMock
|
||||
:ivar vat_rate: Active VAT rate used for testing VAT rate retrieval.
|
||||
:type vat_rate: VatRate
|
||||
"""
|
||||
# :ivar mock_model: Mocked model used to simulate interactions with the
|
||||
# CarFinanceCalculator instance.
|
||||
# :type mock_model: unittest.mock.MagicMock
|
||||
# :ivar vat_rate: Active VAT rate used for testing VAT rate retrieval.
|
||||
# :type vat_rate: VatRate
|
||||
# """
|
||||
|
||||
def setUp(self):
|
||||
# Common setup for all tests
|
||||
self.mock_model = MagicMock()
|
||||
self.vat_rate = VatRate.objects.create(rate=Decimal("0.20"), is_active=True)
|
||||
# def setUp(self):
|
||||
# # Common setup for all tests
|
||||
# self.mock_model = MagicMock()
|
||||
# self.vat_rate = VatRate.objects.create(rate=Decimal("0.20"), is_active=True)
|
||||
|
||||
def test_no_active_vat_rate_raises_error(self):
|
||||
VatRate.objects.all().delete() # Ensure no active VAT
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
CarFinanceCalculator(self.mock_model)
|
||||
# def test_no_active_vat_rate_raises_error(self):
|
||||
# VatRate.objects.all().delete() # Ensure no active VAT
|
||||
# with self.assertRaises(ObjectDoesNotExist):
|
||||
# CarFinanceCalculator(self.mock_model)
|
||||
|
||||
def test_vat_rate_retrieval(self):
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
self.assertEqual(calculator.vat_rate, Decimal("0.20"))
|
||||
# def test_vat_rate_retrieval(self):
|
||||
# calculator = CarFinanceCalculator(self.mock_model)
|
||||
# self.assertEqual(calculator.vat_rate, Decimal("0.20"))
|
||||
|
||||
def test_item_transactions_retrieval(self):
|
||||
mock_item = MagicMock()
|
||||
self.mock_model.get_itemtxs_data.return_value = [
|
||||
MagicMock(all=lambda: [mock_item])
|
||||
]
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
self.assertEqual(calculator.item_transactions, [mock_item])
|
||||
# def test_item_transactions_retrieval(self):
|
||||
# mock_item = MagicMock()
|
||||
# self.mock_model.get_itemtxs_data.return_value = [
|
||||
# MagicMock(all=lambda: [mock_item])
|
||||
# ]
|
||||
# calculator = CarFinanceCalculator(self.mock_model)
|
||||
# self.assertEqual(calculator.item_transactions, [mock_item])
|
||||
|
||||
def test_get_car_data(self):
|
||||
mock_item = MagicMock()
|
||||
mock_item.ce_quantity = 2
|
||||
mock_item.item_model = MagicMock()
|
||||
mock_item.item_model.item_number = "123"
|
||||
mock_item.item_model.additional_info = {
|
||||
CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
"selling_price": "10000",
|
||||
"cost_price": "8000",
|
||||
"discount_amount": "500",
|
||||
"total_vat": "2000",
|
||||
},
|
||||
CarFinanceCalculator.CAR_INFO_KEY: {
|
||||
"vin": "VIN123",
|
||||
"make": "Toyota",
|
||||
"model": "Camry",
|
||||
"year": 2020,
|
||||
"trim": "LE",
|
||||
"mileage": 15000,
|
||||
},
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}
|
||||
],
|
||||
}
|
||||
# def test_get_car_data(self):
|
||||
# mock_item = MagicMock()
|
||||
# mock_item.ce_quantity = 2
|
||||
# mock_item.item_model = MagicMock()
|
||||
# mock_item.item_model.item_number = "123"
|
||||
# mock_item.item_model.additional_info = {
|
||||
# CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
# "selling_price": "10000",
|
||||
# "cost_price": "8000",
|
||||
# "discount_amount": "500",
|
||||
# "total_vat": "2000",
|
||||
# },
|
||||
# CarFinanceCalculator.CAR_INFO_KEY: {
|
||||
# "vin": "VIN123",
|
||||
# "make": "Toyota",
|
||||
# "model": "Camry",
|
||||
# "year": 2020,
|
||||
# "trim": "LE",
|
||||
# "mileage": 15000,
|
||||
# },
|
||||
# CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
# {"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}
|
||||
# ],
|
||||
# }
|
||||
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
car_data = calculator._get_car_data(mock_item)
|
||||
# calculator = CarFinanceCalculator(self.mock_model)
|
||||
# car_data = calculator._get_car_data(mock_item)
|
||||
|
||||
self.assertEqual(car_data["item_number"], "123")
|
||||
self.assertEqual(car_data["vin"], "VIN123")
|
||||
self.assertEqual(car_data["make"], "Toyota")
|
||||
self.assertEqual(car_data["selling_price"], "10000")
|
||||
self.assertEqual(car_data["unit_price"], Decimal("10000"))
|
||||
self.assertEqual(car_data["quantity"], 2)
|
||||
self.assertEqual(car_data["total"], Decimal("20000"))
|
||||
self.assertEqual(car_data["total_vat"], "2000")
|
||||
self.assertEqual(
|
||||
car_data["additional_services"],
|
||||
[{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}],
|
||||
)
|
||||
# self.assertEqual(car_data["item_number"], "123")
|
||||
# self.assertEqual(car_data["vin"], "VIN123")
|
||||
# self.assertEqual(car_data["make"], "Toyota")
|
||||
# self.assertEqual(car_data["selling_price"], "10000")
|
||||
# self.assertEqual(car_data["unit_price"], Decimal("10000"))
|
||||
# self.assertEqual(car_data["quantity"], 2)
|
||||
# self.assertEqual(car_data["total"], Decimal("20000"))
|
||||
# self.assertEqual(car_data["total_vat"], "2000")
|
||||
# self.assertEqual(
|
||||
# car_data["additional_services"],
|
||||
# [{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}],
|
||||
# )
|
||||
|
||||
def test_get_additional_services(self):
|
||||
mock_item1 = MagicMock()
|
||||
mock_item1.item_model.additional_info = {
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{"name": "Service 1", "price": "100", "taxable": True, "price_": "120"}
|
||||
]
|
||||
}
|
||||
mock_item2 = MagicMock()
|
||||
mock_item2.item_model.additional_info = {
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{"name": "Service 2", "price": "200", "taxable": False, "price_": "200"}
|
||||
]
|
||||
}
|
||||
# def test_get_additional_services(self):
|
||||
# mock_item1 = MagicMock()
|
||||
# mock_item1.item_model.additional_info = {
|
||||
# CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
# {"name": "Service 1", "price": "100", "taxable": True, "price_": "120"}
|
||||
# ]
|
||||
# }
|
||||
# mock_item2 = MagicMock()
|
||||
# mock_item2.item_model.additional_info = {
|
||||
# CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
# {"name": "Service 2", "price": "200", "taxable": False, "price_": "200"}
|
||||
# ]
|
||||
# }
|
||||
|
||||
self.mock_model.get_itemtxs_data.return_value = [
|
||||
MagicMock(all=lambda: [mock_item1, mock_item2])
|
||||
]
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
services = calculator._get_additional_services()
|
||||
# self.mock_model.get_itemtxs_data.return_value = [
|
||||
# MagicMock(all=lambda: [mock_item1, mock_item2])
|
||||
# ]
|
||||
# calculator = CarFinanceCalculator(self.mock_model)
|
||||
# services = calculator._get_additional_services()
|
||||
|
||||
self.assertEqual(len(services), 2)
|
||||
self.assertEqual(services[0]["name"], "Service 1")
|
||||
self.assertEqual(services[1]["name"], "Service 2")
|
||||
self.assertEqual(services[0]["price_"], "120")
|
||||
self.assertEqual(services[1]["price_"], "200")
|
||||
# self.assertEqual(len(services), 2)
|
||||
# self.assertEqual(services[0]["name"], "Service 1")
|
||||
# self.assertEqual(services[1]["name"], "Service 2")
|
||||
# self.assertEqual(services[0]["price_"], "120")
|
||||
# self.assertEqual(services[1]["price_"], "200")
|
||||
|
||||
def test_calculate_totals(self):
|
||||
mock_item1 = MagicMock()
|
||||
mock_item1.ce_quantity = 2
|
||||
mock_item1.item_model.additional_info = {
|
||||
CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
"selling_price": "10000",
|
||||
"discount_amount": "500",
|
||||
},
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{"price_": "100"},
|
||||
{"price_": "200"},
|
||||
],
|
||||
}
|
||||
# def test_calculate_totals(self):
|
||||
# mock_item1 = MagicMock()
|
||||
# mock_item1.ce_quantity = 2
|
||||
# mock_item1.item_model.additional_info = {
|
||||
# CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
# "selling_price": "10000",
|
||||
# "discount_amount": "500",
|
||||
# },
|
||||
# CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
# {"price_": "100"},
|
||||
# {"price_": "200"},
|
||||
# ],
|
||||
# }
|
||||
|
||||
mock_item2 = MagicMock()
|
||||
mock_item2.quantity = 3
|
||||
mock_item2.item_model.additional_info = {
|
||||
CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
"selling_price": "20000",
|
||||
"discount_amount": "1000",
|
||||
},
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [{"price_": "300"}],
|
||||
}
|
||||
# mock_item2 = MagicMock()
|
||||
# mock_item2.quantity = 3
|
||||
# mock_item2.item_model.additional_info = {
|
||||
# CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
# "selling_price": "20000",
|
||||
# "discount_amount": "1000",
|
||||
# },
|
||||
# CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [{"price_": "300"}],
|
||||
# }
|
||||
|
||||
self.mock_model.get_itemtxs_data.return_value = [
|
||||
MagicMock(all=lambda: [mock_item1, mock_item2])
|
||||
]
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
totals = calculator.calculate_totals()
|
||||
# self.mock_model.get_itemtxs_data.return_value = [
|
||||
# MagicMock(all=lambda: [mock_item1, mock_item2])
|
||||
# ]
|
||||
# calculator = CarFinanceCalculator(self.mock_model)
|
||||
# totals = calculator.calculate_totals()
|
||||
|
||||
expected_total_price = (Decimal("10000") * 2 + Decimal("20000") * 3) - (
|
||||
Decimal("500") + Decimal("1000")
|
||||
)
|
||||
expected_vat = expected_total_price * Decimal("0.15")
|
||||
expected_additionals = Decimal("100") + Decimal("200") + Decimal("300")
|
||||
expected_grand_total = (
|
||||
expected_total_price + expected_vat + expected_additionals
|
||||
).quantize(Decimal("0.00"))
|
||||
# expected_total_price = (Decimal("10000") * 2 + Decimal("20000") * 3) - (
|
||||
# Decimal("500") + Decimal("1000")
|
||||
# )
|
||||
# expected_vat = expected_total_price * Decimal("0.15")
|
||||
# expected_additionals = Decimal("100") + Decimal("200") + Decimal("300")
|
||||
# expected_grand_total = (
|
||||
# expected_total_price + expected_vat + expected_additionals
|
||||
# ).quantize(Decimal("0.00"))
|
||||
|
||||
self.assertEqual(totals["total_price"], expected_total_price)
|
||||
self.assertEqual(totals["total_discount"], Decimal("1500"))
|
||||
self.assertEqual(totals["total_vat_amount"], expected_vat)
|
||||
self.assertEqual(totals["total_additionals"], expected_additionals)
|
||||
self.assertEqual(totals["grand_total"], expected_grand_total)
|
||||
# self.assertEqual(totals["total_price"], expected_total_price)
|
||||
# self.assertEqual(totals["total_discount"], Decimal("1500"))
|
||||
# self.assertEqual(totals["total_vat_amount"], expected_vat)
|
||||
# self.assertEqual(totals["total_additionals"], expected_additionals)
|
||||
# self.assertEqual(totals["grand_total"], expected_grand_total)
|
||||
|
||||
def test_get_finance_data(self):
|
||||
mock_item = MagicMock()
|
||||
mock_item.ce_quantity = 1
|
||||
mock_item.item_model = MagicMock()
|
||||
mock_item.item_model.item_number = "456"
|
||||
mock_item.item_model.additional_info = {
|
||||
CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
"selling_price": "15000",
|
||||
"discount_amount": "1000",
|
||||
"total_vat": "2800",
|
||||
},
|
||||
CarFinanceCalculator.CAR_INFO_KEY: {
|
||||
"vin": "VIN456",
|
||||
"make": "Honda",
|
||||
"model": "Civic",
|
||||
"year": 2021,
|
||||
},
|
||||
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
{"name": "Service", "price": "150", "taxable": True, "price_": "180"}
|
||||
],
|
||||
}
|
||||
# def test_get_finance_data(self):
|
||||
# mock_item = MagicMock()
|
||||
# mock_item.ce_quantity = 1
|
||||
# mock_item.item_model = MagicMock()
|
||||
# mock_item.item_model.item_number = "456"
|
||||
# mock_item.item_model.additional_info = {
|
||||
# CarFinanceCalculator.CAR_FINANCE_KEY: {
|
||||
# "selling_price": "15000",
|
||||
# "discount_amount": "1000",
|
||||
# "total_vat": "2800",
|
||||
# },
|
||||
# CarFinanceCalculator.CAR_INFO_KEY: {
|
||||
# "vin": "VIN456",
|
||||
# "make": "Honda",
|
||||
# "model": "Civic",
|
||||
# "year": 2021,
|
||||
# },
|
||||
# CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
|
||||
# {"name": "Service", "price": "150", "taxable": True, "price_": "180"}
|
||||
# ],
|
||||
# }
|
||||
|
||||
self.mock_model.get_itemtxs_data.return_value = [
|
||||
MagicMock(all=lambda: [mock_item])
|
||||
]
|
||||
calculator = CarFinanceCalculator(self.mock_model)
|
||||
finance_data = calculator.get_finance_data()
|
||||
# self.mock_model.get_itemtxs_data.return_value = [
|
||||
# MagicMock(all=lambda: [mock_item])
|
||||
# ]
|
||||
# calculator = CarFinanceCalculator(self.mock_model)
|
||||
# finance_data = calculator.get_finance_data()
|
||||
|
||||
self.assertEqual(len(finance_data["cars"]), 1)
|
||||
self.assertEqual(finance_data["quantity"], 1)
|
||||
self.assertEqual(finance_data["total_price"], Decimal("14000")) # 15000 - 1000
|
||||
self.assertEqual(
|
||||
finance_data["total_vat"],
|
||||
Decimal("14000") + (Decimal("14000") * Decimal("0.20")),
|
||||
)
|
||||
self.assertEqual(
|
||||
finance_data["total_vat_amount"], Decimal("14000") * Decimal("0.20")
|
||||
)
|
||||
self.assertEqual(finance_data["total_additionals"], Decimal("180"))
|
||||
self.assertEqual(finance_data["additionals"][0]["name"], "Service")
|
||||
self.assertEqual(finance_data["vat"], Decimal("0.20"))
|
||||
# self.assertEqual(len(finance_data["cars"]), 1)
|
||||
# self.assertEqual(finance_data["quantity"], 1)
|
||||
# self.assertEqual(finance_data["total_price"], Decimal("14000")) # 15000 - 1000
|
||||
# self.assertEqual(
|
||||
# finance_data["total_vat"],
|
||||
# Decimal("14000") + (Decimal("14000") * Decimal("0.20")),
|
||||
# )
|
||||
# self.assertEqual(
|
||||
# finance_data["total_vat_amount"], Decimal("14000") * Decimal("0.20")
|
||||
# )
|
||||
# self.assertEqual(finance_data["total_additionals"], Decimal("180"))
|
||||
# self.assertEqual(finance_data["additionals"][0]["name"], "Service")
|
||||
# self.assertEqual(finance_data["vat"], Decimal("0.20"))
|
||||
|
||||
|
||||
|
||||
@ -1368,14 +1368,11 @@ def create_make_accounts(dealer):
|
||||
|
||||
def handle_payment(request, order):
|
||||
url = "https://api.moyasar.com/v1/payments"
|
||||
callback_url = request.build_absolute_uri(reverse("payment_callback", args=[request.dealer.slug]))
|
||||
callback_url = request.build_absolute_uri(
|
||||
reverse("payment_callback", kwargs={"dealer_slug": request.dealer.slug})
|
||||
)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
# email = request.user.email
|
||||
# first_name = request.user.first_name
|
||||
# last_name = request.user.last_name
|
||||
# phone = request.user.phone
|
||||
# else:
|
||||
email = request.POST["email"]
|
||||
first_name = request.POST["first_name"]
|
||||
last_name = request.POST["last_name"]
|
||||
@ -1430,12 +1427,13 @@ def handle_payment(request, order):
|
||||
else:
|
||||
print("Failed to process payment:", data)
|
||||
#
|
||||
order.status = AbstractOrder.STATUS.NEW
|
||||
data = response.json()
|
||||
print(data)
|
||||
# order.status = AbstractOrder.STATUS.NEW
|
||||
order.save()
|
||||
#
|
||||
data = response.json()
|
||||
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
|
||||
print(data)
|
||||
|
||||
models.PaymentHistory.objects.create(
|
||||
user=request.user,
|
||||
user_data=user_data,
|
||||
@ -1447,7 +1445,6 @@ def handle_payment(request, order):
|
||||
gateway_response=data,
|
||||
)
|
||||
transaction_url = data["source"]["transaction_url"]
|
||||
|
||||
return transaction_url
|
||||
|
||||
|
||||
|
||||
@ -324,9 +324,7 @@ def dealer_signup(request):
|
||||
if password != password_confirm:
|
||||
return JsonResponse({"error": _("Passwords do not match")}, status=400)
|
||||
try:
|
||||
async_task(create_user_dealer(
|
||||
email, password, name, arabic_name, phone, crn, vrn, address
|
||||
))
|
||||
async_task(create_user_dealer,email, password, name, arabic_name, phone, crn, vrn, address)
|
||||
logger.info(f"Delear created succesfully with emailID {email}")
|
||||
return JsonResponse({"message": _("User created successfully")}, status=200)
|
||||
except Exception as e:
|
||||
@ -608,7 +606,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
dealer = get_user_type(self.request)
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
|
||||
return form
|
||||
|
||||
@ -4210,15 +4208,13 @@ def sales_list_view(request, dealer_slug):
|
||||
staff = getattr(request.user.staffmember, "staff", None)
|
||||
qs = []
|
||||
try:
|
||||
if request.is_dealer:
|
||||
if any([request.is_dealer, request.is_manager, request.is_accountant]):
|
||||
qs = models.ExtraInfo.get_sale_orders(staff=staff,is_dealer=True)
|
||||
elif request.is_staff:
|
||||
qs = models.ExtraInfo.get_sale_orders(staff=staff)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
# sale_orders = models.SaleOrder.objects.filter(
|
||||
# dealer=dealer,
|
||||
# )
|
||||
|
||||
paginator = Paginator(qs, 30)
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
@ -4291,12 +4287,12 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
||||
|
||||
if self.request.is_dealer:
|
||||
if any([self.request.is_dealer ,self.request.is_manager ,self.request.is_accountant]):
|
||||
qs = models.ExtraInfo.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
related_content_type=ContentType.objects.get_for_model(models.Staff),
|
||||
)
|
||||
elif self.request.is_staff:
|
||||
elif self.request.is_staff and self.request.is_sales:
|
||||
qs = models.ExtraInfo.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
related_content_type=ContentType.objects.get_for_model(models.Staff),
|
||||
@ -4860,7 +4856,7 @@ def estimate_mark_as(request, dealer_slug, pk):
|
||||
estimate.save()
|
||||
#Reserve The Car
|
||||
car = estimate.get_itemtxs_data()[0].first().item_model.car
|
||||
reserve_car(item_instance.car, request)
|
||||
reserve_car(car, request)
|
||||
messages.success(request, _("Quotation approved successfully"))
|
||||
return redirect(
|
||||
"estimate_list", dealer_slug=dealer.slug
|
||||
@ -4939,7 +4935,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
||||
qs = []
|
||||
try:
|
||||
if self.request.is_dealer:
|
||||
if any([self.request.is_dealer, self.request.is_manager,self.request.is_accountant]):
|
||||
qs = models.ExtraInfo.get_invoices(staff=staff,is_dealer=True)
|
||||
elif self.request.is_staff:
|
||||
qs = models.ExtraInfo.get_invoices(staff=staff)
|
||||
@ -5587,8 +5583,8 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
qs = apply_search_filters(qs, query)
|
||||
if self.request.is_dealer:
|
||||
return qs
|
||||
staffmember = getattr(self.request.user, "staffmember", None)
|
||||
if staff := getattr(staffmember, "staff", None):
|
||||
if self.request.user.is_staff:
|
||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
||||
return qs.filter(staff=staff)
|
||||
return models.Lead.objects.none()
|
||||
|
||||
@ -7540,7 +7536,6 @@ class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
def get_queryset(self):
|
||||
dealer = get_user_type(self.request)
|
||||
qs = super().get_queryset()
|
||||
print(qs)
|
||||
return qs.filter(estimate__entity=dealer.entity)
|
||||
|
||||
|
||||
@ -9107,16 +9102,22 @@ def submit_plan(request,dealer_slug):
|
||||
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
||||
selected_plan_id = request.POST.get("selected_plan")
|
||||
pp = PlanPricing.objects.get(pk=selected_plan_id)
|
||||
|
||||
order = Order.objects.create(
|
||||
user=dealer.user,
|
||||
plan=pp.plan,
|
||||
pricing=pp.pricing,
|
||||
amount=pp.price,
|
||||
currency=settings.DEFAULT_CURRENCY,
|
||||
tax=15,
|
||||
status=AbstractOrder.STATUS.NEW,
|
||||
)
|
||||
order = None
|
||||
try:
|
||||
order = Order.objects.create(
|
||||
user=dealer.user,
|
||||
plan=pp.plan,
|
||||
pricing=pp.pricing,
|
||||
amount=pp.price,
|
||||
currency="SA",
|
||||
tax=15,
|
||||
status=1,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
if not order:
|
||||
messages.error(request, _("Error creating order"))
|
||||
return redirect("pricing_page", dealer_slug=dealer_slug)
|
||||
transaction_url = handle_payment(request, order)
|
||||
return redirect(transaction_url)
|
||||
|
||||
@ -9130,7 +9131,7 @@ def payment_callback(request,dealer_slug):
|
||||
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
|
||||
payment_status = request.GET.get("status")
|
||||
order = Order.objects.filter(
|
||||
user=dealer.user, status=AbstractOrder.STATUS.NEW
|
||||
user=dealer.user, status=1
|
||||
).first()
|
||||
|
||||
if payment_status == "paid":
|
||||
@ -9818,6 +9819,7 @@ def InventoryItemCreateView(request, dealer_slug):
|
||||
# return redirect("purchase_order_list", dealer_slug=dealer.slug)
|
||||
if for_po:
|
||||
form = forms.CSVUploadForm()
|
||||
form.fields["year"].widget.attrs["hx-get"] = reverse("inventory_items_filter", kwargs={"dealer_slug": dealer.slug})
|
||||
form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
|
||||
context = {
|
||||
"make_data": models.CarMake.objects.filter(is_sa_import=True),
|
||||
@ -9840,6 +9842,7 @@ def InventoryItemCreateView(request, dealer_slug):
|
||||
@permission_required("django_ledger.view_purchaseordermodel", raise_exception=True)
|
||||
def inventory_items_filter(request, dealer_slug):
|
||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||
year = request.GET.get("year", None)
|
||||
make = request.GET.get("make")
|
||||
model = request.GET.get("model")
|
||||
serie = request.GET.get("serie")
|
||||
@ -9853,6 +9856,9 @@ def inventory_items_filter(request, dealer_slug):
|
||||
elif model:
|
||||
model = models.CarModel.objects.get(pk=model)
|
||||
serie_data = model.carserie_set.all()
|
||||
if year:
|
||||
serie_data = serie_data.filter(year_begin__lte=year, year_end__gte=year)
|
||||
print(serie_data)
|
||||
elif serie:
|
||||
serie = models.CarSerie.objects.get(pk=serie)
|
||||
trim_data = serie.cartrim_set.all()
|
||||
@ -9860,8 +9866,6 @@ def inventory_items_filter(request, dealer_slug):
|
||||
"model_data": model_data,
|
||||
"serie_data": serie_data,
|
||||
"trim_data": trim_data,
|
||||
# 'inventory_items': dealer.entity.get_items_inventory(),
|
||||
# 'entity_slug': dealer.entity.slug,
|
||||
}
|
||||
return render(request, "purchase_orders/car_inventory_item_form.html", context)
|
||||
|
||||
|
||||
@ -109,3 +109,25 @@ html[dir="rtl"] .form-icon-container .form-control {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.submitBtn.loading {
|
||||
position: relative;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.submitBtn.loading:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@ -1,3 +1,29 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Find all forms with class 'disable-on-submit' or target a specific form
|
||||
const forms = document.querySelectorAll('form');
|
||||
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Find the submit button within this form
|
||||
const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
|
||||
|
||||
if (submitButton) {
|
||||
// Disable the button
|
||||
submitButton.disabled = true;
|
||||
|
||||
// Optional: Add a loading class for styling
|
||||
submitButton.classList.add('loading');
|
||||
|
||||
// Re-enable the button if the form submission fails
|
||||
// This ensures the button doesn't stay disabled if there's an error
|
||||
window.addEventListener('unload', function() {
|
||||
submitButton.disabled = false;
|
||||
submitButton.classList.remove('loading');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
@ -222,3 +248,4 @@ const getDataTableInit = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -1121,6 +1121,7 @@ a.deletelink:focus, a.deletelink:hover {
|
||||
margin: 0;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.paginator a:link, .paginator a:visited {
|
||||
|
||||
@ -84,8 +84,8 @@ html[data-theme="dark"] {
|
||||
|
||||
.theme-toggle svg {
|
||||
vertical-align: middle;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@ -449,17 +449,6 @@ body.popup .submit-row {
|
||||
_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);
|
||||
@ -473,11 +462,8 @@ body.popup .submit-row {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -254,10 +254,6 @@ input[type="submit"], button {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selector .selector-filter label {
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.selector .selector-filter input {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
@ -277,29 +273,7 @@ input[type="submit"], button {
|
||||
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 {
|
||||
.selector-chooseall, .selector-clearall {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@ -321,8 +295,6 @@ input[type="submit"], button {
|
||||
}
|
||||
|
||||
.stacked ul.selector-chooser {
|
||||
width: 52px;
|
||||
height: 26px;
|
||||
padding: 0 2px;
|
||||
transform: none;
|
||||
}
|
||||
@ -331,42 +303,6 @@ input[type="submit"], button {
|
||||
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;
|
||||
}
|
||||
@ -649,6 +585,7 @@ input[type="submit"], button {
|
||||
|
||||
.related-widget-wrapper .selector {
|
||||
order: 1;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.related-widget-wrapper > a {
|
||||
@ -679,9 +616,9 @@ input[type="submit"], button {
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
display: block;
|
||||
width: 52px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
padding: 0 2px;
|
||||
transform: none;
|
||||
}
|
||||
@ -694,16 +631,16 @@ input[type="submit"], button {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -20px;
|
||||
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
||||
background-position: 0 -24px;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background-position: 0 -40px;
|
||||
background-position: 0 -48px;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -60px;
|
||||
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
||||
background-position: 0 -72px;
|
||||
}
|
||||
|
||||
/* Inlines */
|
||||
|
||||
@ -28,18 +28,12 @@
|
||||
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;
|
||||
}
|
||||
@ -53,22 +47,6 @@
|
||||
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 */
|
||||
@ -97,15 +75,15 @@
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -20px;
|
||||
[dir="rtl"] :enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
||||
background-position: 0 -24px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .selector-add {
|
||||
background-position: 0 -40px;
|
||||
background-position: 0 -48px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -60px;
|
||||
[dir="rtl"] :enabled.selector-add:focus, :enabled.selector-add:hover {
|
||||
background-position: 0 -72px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,34 +220,36 @@ fieldset .fieldBox {
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
|
||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
||||
background-size: 24px auto;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -80px;
|
||||
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
||||
background-position: 0 -120px;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
||||
background: url(../img/selector-icons.svg) 0 -144px no-repeat;
|
||||
background-size: 24px auto;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -112px;
|
||||
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
||||
background-position: 0 -168px;
|
||||
}
|
||||
|
||||
a.selector-chooseall {
|
||||
.selector-chooseall {
|
||||
background: url(../img/selector-icons.svg) right -128px no-repeat;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
|
||||
:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover {
|
||||
background-position: 100% -144px;
|
||||
}
|
||||
|
||||
a.selector-clearall {
|
||||
.selector-clearall {
|
||||
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
|
||||
}
|
||||
|
||||
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
||||
:enabled.selector-clearall:focus, :enabled.selector-clearall:hover {
|
||||
background-position: 0 -176px;
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
.selector {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex: 1;
|
||||
gap: 0 10px;
|
||||
}
|
||||
|
||||
@ -14,17 +14,20 @@
|
||||
}
|
||||
|
||||
.selector-available, .selector-chosen {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.selector-available h2, .selector-chosen h2 {
|
||||
.selector-available-title, .selector-chosen-title {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.selector .helptext {
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.selector-chosen .list-footer-display {
|
||||
border: 1px solid var(--border-color);
|
||||
border-top: none;
|
||||
@ -40,14 +43,25 @@
|
||||
color: var(--breadcrumbs-fg);
|
||||
}
|
||||
|
||||
.selector-chosen h2 {
|
||||
.selector-chosen-title {
|
||||
background: var(--secondary);
|
||||
color: var(--header-link-color);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.selector .selector-available h2 {
|
||||
.aligned .selector-chosen-title label {
|
||||
color: var(--header-link-color);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector-available-title {
|
||||
background: var(--darkened-bg);
|
||||
color: var(--body-quiet-color);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.aligned .selector-available-title label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector .selector-filter {
|
||||
@ -59,6 +73,7 @@
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selector .selector-filter label,
|
||||
@ -77,14 +92,9 @@
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.selector .selector-available input,
|
||||
.selector .selector-chosen input {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
align-self: center;
|
||||
width: 22px;
|
||||
width: 30px;
|
||||
background-color: var(--selected-bg);
|
||||
border-radius: 10px;
|
||||
margin: 0;
|
||||
@ -114,40 +124,43 @@
|
||||
}
|
||||
|
||||
.selector-add, .selector-remove {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
text-indent: -3000px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
opacity: 0.55;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.active.selector-add, .active.selector-remove {
|
||||
:enabled.selector-add, :enabled.selector-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.active.selector-add:hover, .active.selector-remove:hover {
|
||||
:enabled.selector-add:hover, :enabled.selector-remove:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
||||
background: url(../img/selector-icons.svg) 0 -144px no-repeat;
|
||||
background-size: 24px auto;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -112px;
|
||||
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
||||
background-position: 0 -168px;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
|
||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
||||
background-size: 24px auto;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -80px;
|
||||
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
||||
background-position: 0 -120px;
|
||||
}
|
||||
|
||||
a.selector-chooseall, a.selector-clearall {
|
||||
.selector-chooseall, .selector-clearall {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
text-align: left;
|
||||
@ -158,38 +171,39 @@ a.selector-chooseall, a.selector-clearall {
|
||||
color: var(--body-quiet-color);
|
||||
text-decoration: none;
|
||||
opacity: 0.55;
|
||||
border: none;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
|
||||
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
||||
:enabled.selector-chooseall:focus, :enabled.selector-clearall:focus,
|
||||
:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover {
|
||||
color: var(--link-fg);
|
||||
}
|
||||
|
||||
a.active.selector-chooseall, a.active.selector-clearall {
|
||||
:enabled.selector-chooseall, :enabled.selector-clearall {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
||||
:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.selector-chooseall {
|
||||
.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 {
|
||||
:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover {
|
||||
background-position: 100% -176px;
|
||||
}
|
||||
|
||||
a.selector-clearall {
|
||||
.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 {
|
||||
:enabled.selector-clearall:focus, :enabled.selector-clearall:hover {
|
||||
background-position: 0 -144px;
|
||||
}
|
||||
|
||||
@ -219,8 +233,9 @@ a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
||||
}
|
||||
|
||||
.stacked ul.selector-chooser {
|
||||
height: 22px;
|
||||
width: 50px;
|
||||
display: flex;
|
||||
height: 30px;
|
||||
width: 64px;
|
||||
margin: 0 0 10px 40%;
|
||||
background-color: #eee;
|
||||
border-radius: 10px;
|
||||
@ -237,32 +252,34 @@ a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
||||
}
|
||||
|
||||
.stacked .selector-add {
|
||||
background: url(../img/selector-icons.svg) 0 -32px no-repeat;
|
||||
background: url(../img/selector-icons.svg) 0 -48px no-repeat;
|
||||
background-size: 24px auto;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add {
|
||||
background-position: 0 -32px;
|
||||
.stacked :enabled.selector-add {
|
||||
background-position: 0 -48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
|
||||
background-position: 0 -48px;
|
||||
.stacked :enabled.selector-add:focus, .stacked :enabled.selector-add:hover {
|
||||
background-position: 0 -72px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .selector-remove {
|
||||
background: url(../img/selector-icons.svg) 0 0 no-repeat;
|
||||
background-size: 24px auto;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove {
|
||||
.stacked :enabled.selector-remove {
|
||||
background-position: 0 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
|
||||
background-position: 0 -16px;
|
||||
.stacked :enabled.selector-remove:focus, .stacked :enabled.selector-remove:hover {
|
||||
background-position: 0 -24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -318,28 +335,30 @@ table p.datetime {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.datetimeshortcuts .clock-icon {
|
||||
background: url(../img/icon-clock.svg) 0 0 no-repeat;
|
||||
background-size: 24px auto;
|
||||
}
|
||||
|
||||
.datetimeshortcuts a:focus .clock-icon,
|
||||
.datetimeshortcuts a:hover .clock-icon {
|
||||
background-position: 0 -16px;
|
||||
background-position: 0 -24px;
|
||||
}
|
||||
|
||||
.datetimeshortcuts .date-icon {
|
||||
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
|
||||
background-size: 24px auto;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.datetimeshortcuts a:focus .date-icon,
|
||||
.datetimeshortcuts a:hover .date-icon {
|
||||
background-position: 0 -16px;
|
||||
background-position: 0 -24px;
|
||||
}
|
||||
|
||||
.timezonewarning {
|
||||
@ -558,9 +577,10 @@ ul.timelist, .timelist li {
|
||||
float: right;
|
||||
text-indent: -9999px;
|
||||
background: url(../img/inline-delete.svg) 0 0 no-repeat;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 0px none;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.inline-deletelink:focus, .inline-deletelink:hover {
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#999999" 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>
|
||||
|
||||
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 537 B |
@ -15,6 +15,7 @@ Requires core.js and SelectBox.js.
|
||||
const from_box = document.getElementById(field_id);
|
||||
from_box.id += '_from'; // change its ID
|
||||
from_box.className = 'filtered';
|
||||
from_box.setAttribute('aria-labelledby', field_id + '_from_title');
|
||||
|
||||
for (const p of from_box.parentNode.getElementsByTagName('p')) {
|
||||
if (p.classList.contains("info")) {
|
||||
@ -38,18 +39,15 @@ Requires core.js and SelectBox.js.
|
||||
// <div class="selector-available">
|
||||
const selector_available = quickElement('div', selector_div);
|
||||
selector_available.className = 'selector-available';
|
||||
const title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
|
||||
const selector_available_title = quickElement('div', selector_available);
|
||||
selector_available_title.id = field_id + '_from_title';
|
||||
selector_available_title.className = 'selector-available-title';
|
||||
quickElement('label', selector_available_title, interpolate(gettext('Available %s') + ' ', [field_name]), 'for', field_id + '_from');
|
||||
quickElement(
|
||||
'span', title_available, '',
|
||||
'class', 'help help-tooltip help-icon',
|
||||
'title', interpolate(
|
||||
gettext(
|
||||
'This is the list of available %s. You may choose some by ' +
|
||||
'selecting them in the box below and then clicking the ' +
|
||||
'"Choose" arrow between the two boxes.'
|
||||
),
|
||||
[field_name]
|
||||
)
|
||||
'p',
|
||||
selector_available_title,
|
||||
interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
|
||||
'class', 'helptext'
|
||||
);
|
||||
|
||||
const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
|
||||
@ -60,7 +58,7 @@ Requires core.js and SelectBox.js.
|
||||
quickElement(
|
||||
'span', search_filter_label, '',
|
||||
'class', 'help-tooltip search-label-icon',
|
||||
'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
|
||||
'aria-label', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
|
||||
);
|
||||
|
||||
filter_p.appendChild(document.createTextNode(' '));
|
||||
@ -69,32 +67,47 @@ Requires core.js and SelectBox.js.
|
||||
filter_input.id = field_id + '_input';
|
||||
|
||||
selector_available.appendChild(from_box);
|
||||
const choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link');
|
||||
choose_all.className = 'selector-chooseall';
|
||||
const choose_all = quickElement(
|
||||
'button',
|
||||
selector_available,
|
||||
interpolate(gettext('Choose all %s'), [field_name]),
|
||||
'id', field_id + '_add_all',
|
||||
'class', 'selector-chooseall',
|
||||
'type', 'button'
|
||||
);
|
||||
|
||||
// <ul class="selector-chooser">
|
||||
const selector_chooser = quickElement('ul', selector_div);
|
||||
selector_chooser.className = 'selector-chooser';
|
||||
const add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link');
|
||||
add_link.className = 'selector-add';
|
||||
const remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link');
|
||||
remove_link.className = 'selector-remove';
|
||||
const add_button = quickElement(
|
||||
'button',
|
||||
quickElement('li', selector_chooser),
|
||||
interpolate(gettext('Choose selected %s'), [field_name]),
|
||||
'id', field_id + '_add',
|
||||
'class', 'selector-add',
|
||||
'type', 'button'
|
||||
);
|
||||
const remove_button = quickElement(
|
||||
'button',
|
||||
quickElement('li', selector_chooser),
|
||||
interpolate(gettext('Remove selected %s'), [field_name]),
|
||||
'id', field_id + '_remove',
|
||||
'class', 'selector-remove',
|
||||
'type', 'button'
|
||||
);
|
||||
|
||||
// <div class="selector-chosen">
|
||||
const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen');
|
||||
selector_chosen.className = 'selector-chosen';
|
||||
const title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
|
||||
const selector_chosen_title = quickElement('div', selector_chosen);
|
||||
selector_chosen_title.className = 'selector-chosen-title';
|
||||
selector_chosen_title.id = field_id + '_to_title';
|
||||
quickElement('label', selector_chosen_title, interpolate(gettext('Chosen %s') + ' ', [field_name]), 'for', field_id + '_to');
|
||||
quickElement(
|
||||
'span', title_chosen, '',
|
||||
'class', 'help help-tooltip help-icon',
|
||||
'title', interpolate(
|
||||
gettext(
|
||||
'This is the list of chosen %s. You may remove some by ' +
|
||||
'selecting them in the box below and then clicking the ' +
|
||||
'"Remove" arrow between the two boxes.'
|
||||
),
|
||||
[field_name]
|
||||
)
|
||||
'p',
|
||||
selector_chosen_title,
|
||||
interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
|
||||
'class', 'helptext'
|
||||
);
|
||||
|
||||
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
|
||||
@ -105,7 +118,7 @@ Requires core.js and SelectBox.js.
|
||||
quickElement(
|
||||
'span', search_filter_selected_label, '',
|
||||
'class', 'help-tooltip search-label-icon',
|
||||
'title', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name])
|
||||
'aria-label', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name])
|
||||
);
|
||||
|
||||
filter_selected_p.appendChild(document.createTextNode(' '));
|
||||
@ -113,21 +126,34 @@ Requires core.js and SelectBox.js.
|
||||
const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
|
||||
filter_selected_input.id = field_id + '_selected_input';
|
||||
|
||||
const to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', '', 'size', from_box.size, 'name', from_box.name);
|
||||
to_box.className = 'filtered';
|
||||
|
||||
quickElement(
|
||||
'select',
|
||||
selector_chosen,
|
||||
'',
|
||||
'id', field_id + '_to',
|
||||
'multiple', '',
|
||||
'size', from_box.size,
|
||||
'name', from_box.name,
|
||||
'aria-labelledby', field_id + '_to_title',
|
||||
'class', 'filtered'
|
||||
);
|
||||
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
|
||||
quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text');
|
||||
quickElement('span', warning_footer, ' (click to clear)', 'class', 'list-footer-display__clear');
|
||||
|
||||
const clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link');
|
||||
clear_all.className = 'selector-clearall';
|
||||
quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear');
|
||||
const clear_all = quickElement(
|
||||
'button',
|
||||
selector_chosen,
|
||||
interpolate(gettext('Remove all %s'), [field_name]),
|
||||
'id', field_id + '_remove_all',
|
||||
'class', 'selector-clearall',
|
||||
'type', 'button'
|
||||
);
|
||||
|
||||
from_box.name = from_box.name + '_old';
|
||||
|
||||
// Set up the JavaScript event handlers for the select box filter interface
|
||||
const move_selection = function(e, elem, move_func, from, to) {
|
||||
if (elem.classList.contains('active')) {
|
||||
if (!elem.hasAttribute('disabled')) {
|
||||
move_func(from, to);
|
||||
SelectFilter.refresh_icons(field_id);
|
||||
SelectFilter.refresh_filtered_selects(field_id);
|
||||
@ -138,10 +164,10 @@ Requires core.js and SelectBox.js.
|
||||
choose_all.addEventListener('click', function(e) {
|
||||
move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
|
||||
});
|
||||
add_link.addEventListener('click', function(e) {
|
||||
add_button.addEventListener('click', function(e) {
|
||||
move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
|
||||
});
|
||||
remove_link.addEventListener('click', function(e) {
|
||||
remove_button.addEventListener('click', function(e) {
|
||||
move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
|
||||
});
|
||||
clear_all.addEventListener('click', function(e) {
|
||||
@ -226,13 +252,12 @@ Requires core.js and SelectBox.js.
|
||||
refresh_icons: function(field_id) {
|
||||
const from = document.getElementById(field_id + '_from');
|
||||
const to = document.getElementById(field_id + '_to');
|
||||
// Active if at least one item is selected
|
||||
document.getElementById(field_id + '_add_link').classList.toggle('active', SelectFilter.any_selected(from));
|
||||
document.getElementById(field_id + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to));
|
||||
// Active if the corresponding box isn't empty
|
||||
document.getElementById(field_id + '_add_all_link').classList.toggle('active', from.querySelector('option'));
|
||||
document.getElementById(field_id + '_remove_all_link').classList.toggle('active', to.querySelector('option'));
|
||||
SelectFilter.refresh_filtered_warning(field_id);
|
||||
// Disabled if no items are selected.
|
||||
document.getElementById(field_id + '_add').disabled = !SelectFilter.any_selected(from);
|
||||
document.getElementById(field_id + '_remove').disabled = !SelectFilter.any_selected(to);
|
||||
// Disabled if the corresponding box is empty.
|
||||
document.getElementById(field_id + '_add_all').disabled = !from.querySelector('option');
|
||||
document.getElementById(field_id + '_remove_all').disabled = !to.querySelector('option');
|
||||
},
|
||||
filter_key_press: function(event, field_id, source, target) {
|
||||
const source_box = document.getElementById(field_id + source);
|
||||
|
||||
@ -55,8 +55,9 @@
|
||||
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
||||
elem.value += ',' + chosenId;
|
||||
} else {
|
||||
document.getElementById(name).value = chosenId;
|
||||
elem.value = chosenId;
|
||||
}
|
||||
$(elem).trigger('change');
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
@ -87,7 +88,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId) {
|
||||
function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId, skipIds = []) {
|
||||
// After create/edit a model from the options next to the current
|
||||
// select (+ or :pencil:) update ForeignKey PK of the rest of selects
|
||||
// in the page.
|
||||
@ -100,7 +101,7 @@
|
||||
const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`);
|
||||
|
||||
selectsRelated.forEach(function(select) {
|
||||
if (currentSelect === select) {
|
||||
if (currentSelect === select || skipIds && skipIds.includes(select.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -109,6 +110,11 @@
|
||||
if (!option) {
|
||||
option = new Option(newRepr, newId);
|
||||
select.options.add(option);
|
||||
// Update SelectBox cache for related fields.
|
||||
if (window.SelectBox !== undefined && !SelectBox.cache[currentSelect.id]) {
|
||||
SelectBox.add_to_cache(select.id, option);
|
||||
SelectBox.redisplay(select.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -136,9 +142,14 @@
|
||||
$(elem).trigger('change');
|
||||
} else {
|
||||
const toId = name + "_to";
|
||||
const toElem = document.getElementById(toId);
|
||||
const o = new Option(newRepr, newId);
|
||||
SelectBox.add_to_cache(toId, o);
|
||||
SelectBox.redisplay(toId);
|
||||
if (toElem && toElem.nodeName.toUpperCase() === 'SELECT') {
|
||||
const skipIds = [name + "_from"];
|
||||
updateRelatedSelectsOptions(toElem, win, null, newRepr, newId, skipIds);
|
||||
}
|
||||
}
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
@ -195,6 +206,7 @@
|
||||
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
|
||||
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
|
||||
window.dismissChildPopups = dismissChildPopups;
|
||||
window.relatedWindows = relatedWindows;
|
||||
|
||||
// Kept for backward compatibility
|
||||
window.showAddAnotherPopup = showRelatedObjectPopup;
|
||||
|
||||
@ -50,11 +50,11 @@
|
||||
// If forms are laid out as table rows, insert the
|
||||
// "add" button in a new table row:
|
||||
const numCols = $this.eq(-1).children().length;
|
||||
$parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="#">' + options.addText + "</a></tr>");
|
||||
$parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a role="button" class="addlink" href="#">' + options.addText + "</a></tr>");
|
||||
addButton = $parent.find("tr:last a");
|
||||
} else {
|
||||
// Otherwise, insert it immediately after the last form:
|
||||
$this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="#">' + options.addText + "</a></div>");
|
||||
$this.filter(":last").after('<div class="' + options.addCssClass + '"><a role="button" class="addlink" href="#">' + options.addText + "</a></div>");
|
||||
addButton = $this.filter(":last").next().find("a");
|
||||
}
|
||||
}
|
||||
@ -104,15 +104,15 @@
|
||||
if (row.is("tr")) {
|
||||
// If the forms are laid out in table rows, insert
|
||||
// the remove button into the last table cell:
|
||||
row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
|
||||
row.children(":last").append('<div><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
|
||||
} else if (row.is("ul") || row.is("ol")) {
|
||||
// If they're laid out as an ordered/unordered list,
|
||||
// insert an <li> after the last list item:
|
||||
row.append('<li><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
|
||||
row.append('<li><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
|
||||
} else {
|
||||
// Otherwise, just insert the remove button as the
|
||||
// last child element of the form's container:
|
||||
row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
|
||||
row.children(":first").append('<span><a role="button" class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
|
||||
}
|
||||
// Add delete handler for each row.
|
||||
row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this));
|
||||
|
||||
@ -109,3 +109,25 @@ html[dir="rtl"] .form-icon-container .form-control {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.submitBtn.loading {
|
||||
position: relative;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.submitBtn.loading:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
BIN
staticfiles/images/customers/sun_mountain.jpg
Normal file
BIN
staticfiles/images/customers/sun_mountain.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@ -1,3 +1,29 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Find all forms with class 'disable-on-submit' or target a specific form
|
||||
const forms = document.querySelectorAll('form');
|
||||
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Find the submit button within this form
|
||||
const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
|
||||
|
||||
if (submitButton) {
|
||||
// Disable the button
|
||||
submitButton.disabled = true;
|
||||
|
||||
// Optional: Add a loading class for styling
|
||||
submitButton.classList.add('loading');
|
||||
|
||||
// Re-enable the button if the form submission fails
|
||||
// This ensures the button doesn't stay disabled if there's an error
|
||||
window.addEventListener('unload', function() {
|
||||
submitButton.disabled = false;
|
||||
submitButton.classList.remove('loading');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
@ -222,3 +248,4 @@ const getDataTableInit = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
439
staticfiles/schema_graph/main.js
Normal file
439
staticfiles/schema_graph/main.js
Normal file
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row g-2">
|
||||
|
||||
|
||||
|
||||
<!-- Bill Form -->
|
||||
<div class="col-12">
|
||||
@ -18,25 +18,38 @@
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %}
|
||||
<a href="{% url 'bill-detail' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}"
|
||||
class="btn btn-phoenix-secondary w-100 mb-2">
|
||||
<i class="fas fa-arrow-left me-2"></i>{% trans 'Back to Bill Detail' %}
|
||||
</a>
|
||||
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-phoenix-primary w-100 mb-2">
|
||||
<i class="fas fa-save me-2"></i>{% trans 'Save Bill' %}
|
||||
</button>
|
||||
|
||||
<a href="{% url
|
||||
<a href="{% url 'bill-detail' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}"
|
||||
class="btn btn-phoenix-secondary w-100 mb-2">
|
||||
<i class="fas fa-arrow-left me-2"></i>{% trans 'Back to Bill Detail' %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'bill_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-info w-100 mb-2">
|
||||
<i class="fas fa-list me-2"></i>{% trans 'Bill List' %}
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<a href="{% url 'bill_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-info w-100 mb-2">
|
||||
<i class="fas fa-list me-2"></i>{% trans 'Bill List' %}
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Bill Item Formset -->
|
||||
<div class="col-12">
|
||||
{% bill_item_formset_table itemtxs_formset %}
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
<!-- Total Amount -->
|
||||
<td class="text-end">
|
||||
<span class="text-xs font-weight-bold">
|
||||
{% currency_symbol %}{{ f.instance.total_amount | currency_format }}
|
||||
<span>{% currency_symbol %}</span>{{ f.instance.total_amount | currency_format }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
|
||||
@ -78,6 +78,10 @@
|
||||
<div class="col">
|
||||
{% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{form.year.label}}
|
||||
{{form.year}}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %}
|
||||
</div>
|
||||
@ -90,10 +94,6 @@
|
||||
{{form.vendor.label}}
|
||||
{{form.vendor}}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{form.year.label}}
|
||||
{{form.year}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@ -7,8 +7,10 @@
|
||||
hx-target="#form-{{target}}"
|
||||
hx-select="#form-{{target}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-include="#id_year"
|
||||
{% endif %}
|
||||
>
|
||||
<option value="">--------</option>
|
||||
{% for item in data %}
|
||||
<option value="{{ item.pk }}">{{ item.name }}</option>
|
||||
{% endfor %}
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% if perms.django_ledger.change_invoicemodel%}
|
||||
{% if perms.django_ledger.add_payment%}
|
||||
{% if invoice.invoice_status == 'in_review' %}
|
||||
<button id="accept_invoice" class="btn btn-phoenix-secondary" data-bs-toggle="modal" data-bs-target="#confirmModal"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-check-double"></i> {% trans 'Accept' %}</span></button>
|
||||
{% endif %}
|
||||
@ -98,7 +98,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% url 'invoice_preview' request.dealer.slug invoice.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block"><i class="fa-regular fa-eye"></i> {% trans 'Preview' %}</span></a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{invoice.amount_owned}}
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<td class="align-middle white-space-nowrap align-items-center">{{ user.email }}</td>
|
||||
<td class="align-middle white-space-nowrap align-items-center justify-content-center">{{ user.phone_number }}</td>
|
||||
<td>
|
||||
|
||||
|
||||
{% for group in user.groups %}
|
||||
<span class="badge badge-sm bg-primary text-center"><i class="fa-solid fa-scroll"></i> {% trans group.name|title %}</span>
|
||||
{% endfor %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user