fix the value too long issue in django plan

This commit is contained in:
ismail 2025-07-10 13:22:32 +03:00
parent 4b7bf44923
commit 5884a0cb8d
35 changed files with 1098 additions and 523 deletions

View File

@ -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.db.models.deletion
import django.utils.timezone import django.utils.timezone

View File

@ -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.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View File

@ -2035,7 +2035,17 @@ class CSVUploadForm(forms.Form):
) )
year = forms.IntegerField( year = forms.IntegerField(
label=_("Year"), 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, required=True,
) )
exterior = forms.ModelChoiceField( exterior = forms.ModelChoiceField(

View File

@ -96,10 +96,23 @@ class InjectDealerMiddleware:
if request.user.is_authenticated: if request.user.is_authenticated:
request.is_dealer = False request.is_dealer = False
request.is_staff = 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"): if hasattr(request.user, "dealer"):
request.is_dealer = True request.is_dealer = True
elif hasattr(request.user, "staffmember"): elif hasattr(request.user, "staffmember"):
request.is_staff = True 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: except Exception:
pass pass
response = self.get_response(request) response = self.get_response(request)

View File

@ -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 datetime
import django.core.serializers.json
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
@ -21,7 +22,7 @@ class Migration(migrations.Migration):
('appointment', '0001_initial'), ('appointment', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'), ('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'), ('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), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
@ -84,20 +85,6 @@ class Migration(migrations.Migration):
}, },
bases=(models.Model, inventory.mixins.LocalizedNameMixin), 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( migrations.CreateModel(
name='VatRate', name='VatRate',
fields=[ fields=[
@ -600,6 +587,21 @@ class Migration(migrations.Migration):
name='organization', name='organization',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.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( migrations.CreateModel(
name='PoItemsUploaded', name='PoItemsUploaded',
fields=[ fields=[
@ -677,16 +679,19 @@ class Migration(migrations.Migration):
name='Schedule', name='Schedule',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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)), ('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_at', models.DateTimeField()),
('scheduled_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email')], default='Call', max_length=200)), ('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))), ('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
('notes', models.TextField(blank=True, null=True)), ('notes', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)), ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=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')), ('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)), ('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
@ -836,6 +841,24 @@ class Migration(migrations.Migration):
'unique_together': {('dealer', 'car_make')}, '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( migrations.CreateModel(
name='CarColors', name='CarColors',
fields=[ fields=[

View File

@ -1188,10 +1188,7 @@ class Staff(models.Model, LocalizedNameMixin):
@property @property
def groups(self): 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()])
@property
def groups(self):
return [x.customgroup for x in self.user.groups.all()]
def clear_groups(self): def clear_groups(self):
self.remove_superuser_permission() self.remove_superuser_permission()
@ -2568,16 +2565,8 @@ class CustomGroup(models.Model):
pass pass
def set_default_permissions(self): 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() self.clear_permissions()
###################################### ######################################
###################################### ######################################
#MANAGER #MANAGER

View File

@ -171,7 +171,7 @@ def create_ledger_entity(sender, instance, created, **kwargs):
entity.create_uom(name=u[1], unit_abbr=u[0]) entity.create_uom(name=u[1], unit_abbr=u[0])
# Create COA accounts, background task # Create COA accounts, background task
async_task(create_coa_accounts,instance.pk) async_task(create_coa_accounts,instance)
# create_settings(instance.pk) # create_settings(instance.pk)
# create_accounts_for_make(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. :param kwargs: Additional keyword arguments passed by the signal.
:type kwargs: dict :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(): transaction.on_commit(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)
# Create Vendor # 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) # VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)
@receiver(post_save, sender=models.Dealer) # @receiver(post_save, sender=models.Dealer)
def create_make_ledger_accounts(sender, instance, created, **kwargs): # 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 # 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 # 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. # 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. # :param sender: The model class (`Dealer`) that triggered the signal.
:type sender: Type[models.Dealer] # :type sender: Type[models.Dealer]
:param instance: The instance of the `Dealer` model that triggered the signal. # :param instance: The instance of the `Dealer` model that triggered the signal.
:param created: A boolean indicating whether a new `Dealer` instance was created. # :param created: A boolean indicating whether a new `Dealer` instance was created.
:type created: bool # :type created: bool
:param kwargs: Additional keyword arguments passed by the signal. # :param kwargs: Additional keyword arguments passed by the signal.
:return: None # :return: None
""" # """
if created: # if created:
entity = instance.entity # entity = instance.entity
coa = entity.get_default_coa() # coa = entity.get_default_coa()
# for make in models.CarMake.objects.all(): # for make in models.CarMake.objects.all():
# last_account = entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).order_by('-created').first() # last_account = entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).order_by('-created').first()

View File

@ -33,9 +33,8 @@ def create_settings(pk):
) )
def create_coa_accounts(pk): def create_coa_accounts(instance):
with transaction.atomic(): with transaction.atomic():
instance = Dealer.objects.select_for_update().get(pk=pk)
entity = instance.entity entity = instance.entity
coa = entity.get_default_coa() coa = entity.get_default_coa()
@ -1483,3 +1482,21 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
address=address, 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)

View File

@ -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) @register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True)
def bill_item_formset_table(context, item_formset): 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 { return {
"dealer_slug": context["view"].kwargs["dealer_slug"], "dealer_slug": context["view"].kwargs["dealer_slug"],
"entity_slug": context["view"].kwargs["entity_slug"], "entity_slug": context["view"].kwargs["entity_slug"],

View File

@ -281,196 +281,196 @@ class AuthenticationTest(TestCase):
) # Assuming the view returns an error for missing fields ) # Assuming the view returns an error for missing fields
class CarFinanceCalculatorTests(TestCase): # class CarFinanceCalculatorTests(TestCase):
""" # """
Unit tests for the CarFinanceCalculator class. # Unit tests for the CarFinanceCalculator class.
This class contains various test cases to validate the correct functionality # This class contains various test cases to validate the correct functionality
of the CarFinanceCalculator. It includes tests for VAT rate retrieval, # of the CarFinanceCalculator. It includes tests for VAT rate retrieval,
item transactions, car data extraction, calculation of totals, fetching # item transactions, car data extraction, calculation of totals, fetching
additional services, and finance-related data processing. # additional services, and finance-related data processing.
:ivar mock_model: Mocked model used to simulate interactions with the # :ivar mock_model: Mocked model used to simulate interactions with the
CarFinanceCalculator instance. # CarFinanceCalculator instance.
:type mock_model: unittest.mock.MagicMock # :type mock_model: unittest.mock.MagicMock
:ivar vat_rate: Active VAT rate used for testing VAT rate retrieval. # :ivar vat_rate: Active VAT rate used for testing VAT rate retrieval.
:type vat_rate: VatRate # :type vat_rate: VatRate
""" # """
def setUp(self): # def setUp(self):
# Common setup for all tests # # Common setup for all tests
self.mock_model = MagicMock() # self.mock_model = MagicMock()
self.vat_rate = VatRate.objects.create(rate=Decimal("0.20"), is_active=True) # self.vat_rate = VatRate.objects.create(rate=Decimal("0.20"), is_active=True)
def test_no_active_vat_rate_raises_error(self): # def test_no_active_vat_rate_raises_error(self):
VatRate.objects.all().delete() # Ensure no active VAT # VatRate.objects.all().delete() # Ensure no active VAT
with self.assertRaises(ObjectDoesNotExist): # with self.assertRaises(ObjectDoesNotExist):
CarFinanceCalculator(self.mock_model) # CarFinanceCalculator(self.mock_model)
def test_vat_rate_retrieval(self): # def test_vat_rate_retrieval(self):
calculator = CarFinanceCalculator(self.mock_model) # calculator = CarFinanceCalculator(self.mock_model)
self.assertEqual(calculator.vat_rate, Decimal("0.20")) # self.assertEqual(calculator.vat_rate, Decimal("0.20"))
def test_item_transactions_retrieval(self): # def test_item_transactions_retrieval(self):
mock_item = MagicMock() # mock_item = MagicMock()
self.mock_model.get_itemtxs_data.return_value = [ # self.mock_model.get_itemtxs_data.return_value = [
MagicMock(all=lambda: [mock_item]) # MagicMock(all=lambda: [mock_item])
] # ]
calculator = CarFinanceCalculator(self.mock_model) # calculator = CarFinanceCalculator(self.mock_model)
self.assertEqual(calculator.item_transactions, [mock_item]) # self.assertEqual(calculator.item_transactions, [mock_item])
def test_get_car_data(self): # def test_get_car_data(self):
mock_item = MagicMock() # mock_item = MagicMock()
mock_item.ce_quantity = 2 # mock_item.ce_quantity = 2
mock_item.item_model = MagicMock() # mock_item.item_model = MagicMock()
mock_item.item_model.item_number = "123" # mock_item.item_model.item_number = "123"
mock_item.item_model.additional_info = { # mock_item.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: { # CarFinanceCalculator.CAR_FINANCE_KEY: {
"selling_price": "10000", # "selling_price": "10000",
"cost_price": "8000", # "cost_price": "8000",
"discount_amount": "500", # "discount_amount": "500",
"total_vat": "2000", # "total_vat": "2000",
}, # },
CarFinanceCalculator.CAR_INFO_KEY: { # CarFinanceCalculator.CAR_INFO_KEY: {
"vin": "VIN123", # "vin": "VIN123",
"make": "Toyota", # "make": "Toyota",
"model": "Camry", # "model": "Camry",
"year": 2020, # "year": 2020,
"trim": "LE", # "trim": "LE",
"mileage": 15000, # "mileage": 15000,
}, # },
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ # CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"} # {"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}
], # ],
} # }
calculator = CarFinanceCalculator(self.mock_model) # calculator = CarFinanceCalculator(self.mock_model)
car_data = calculator._get_car_data(mock_item) # car_data = calculator._get_car_data(mock_item)
self.assertEqual(car_data["item_number"], "123") # self.assertEqual(car_data["item_number"], "123")
self.assertEqual(car_data["vin"], "VIN123") # self.assertEqual(car_data["vin"], "VIN123")
self.assertEqual(car_data["make"], "Toyota") # self.assertEqual(car_data["make"], "Toyota")
self.assertEqual(car_data["selling_price"], "10000") # self.assertEqual(car_data["selling_price"], "10000")
self.assertEqual(car_data["unit_price"], Decimal("10000")) # self.assertEqual(car_data["unit_price"], Decimal("10000"))
self.assertEqual(car_data["quantity"], 2) # self.assertEqual(car_data["quantity"], 2)
self.assertEqual(car_data["total"], Decimal("20000")) # self.assertEqual(car_data["total"], Decimal("20000"))
self.assertEqual(car_data["total_vat"], "2000") # self.assertEqual(car_data["total_vat"], "2000")
self.assertEqual( # self.assertEqual(
car_data["additional_services"], # car_data["additional_services"],
[{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}], # [{"name": "Service 1", "price": "200", "taxable": True, "price_": "240"}],
) # )
def test_get_additional_services(self): # def test_get_additional_services(self):
mock_item1 = MagicMock() # mock_item1 = MagicMock()
mock_item1.item_model.additional_info = { # mock_item1.item_model.additional_info = {
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ # CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{"name": "Service 1", "price": "100", "taxable": True, "price_": "120"} # {"name": "Service 1", "price": "100", "taxable": True, "price_": "120"}
] # ]
} # }
mock_item2 = MagicMock() # mock_item2 = MagicMock()
mock_item2.item_model.additional_info = { # mock_item2.item_model.additional_info = {
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ # CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{"name": "Service 2", "price": "200", "taxable": False, "price_": "200"} # {"name": "Service 2", "price": "200", "taxable": False, "price_": "200"}
] # ]
} # }
self.mock_model.get_itemtxs_data.return_value = [ # self.mock_model.get_itemtxs_data.return_value = [
MagicMock(all=lambda: [mock_item1, mock_item2]) # MagicMock(all=lambda: [mock_item1, mock_item2])
] # ]
calculator = CarFinanceCalculator(self.mock_model) # calculator = CarFinanceCalculator(self.mock_model)
services = calculator._get_additional_services() # services = calculator._get_additional_services()
self.assertEqual(len(services), 2) # self.assertEqual(len(services), 2)
self.assertEqual(services[0]["name"], "Service 1") # self.assertEqual(services[0]["name"], "Service 1")
self.assertEqual(services[1]["name"], "Service 2") # self.assertEqual(services[1]["name"], "Service 2")
self.assertEqual(services[0]["price_"], "120") # self.assertEqual(services[0]["price_"], "120")
self.assertEqual(services[1]["price_"], "200") # self.assertEqual(services[1]["price_"], "200")
def test_calculate_totals(self): # def test_calculate_totals(self):
mock_item1 = MagicMock() # mock_item1 = MagicMock()
mock_item1.ce_quantity = 2 # mock_item1.ce_quantity = 2
mock_item1.item_model.additional_info = { # mock_item1.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: { # CarFinanceCalculator.CAR_FINANCE_KEY: {
"selling_price": "10000", # "selling_price": "10000",
"discount_amount": "500", # "discount_amount": "500",
}, # },
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ # CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{"price_": "100"}, # {"price_": "100"},
{"price_": "200"}, # {"price_": "200"},
], # ],
} # }
mock_item2 = MagicMock() # mock_item2 = MagicMock()
mock_item2.quantity = 3 # mock_item2.quantity = 3
mock_item2.item_model.additional_info = { # mock_item2.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: { # CarFinanceCalculator.CAR_FINANCE_KEY: {
"selling_price": "20000", # "selling_price": "20000",
"discount_amount": "1000", # "discount_amount": "1000",
}, # },
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [{"price_": "300"}], # CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [{"price_": "300"}],
} # }
self.mock_model.get_itemtxs_data.return_value = [ # self.mock_model.get_itemtxs_data.return_value = [
MagicMock(all=lambda: [mock_item1, mock_item2]) # MagicMock(all=lambda: [mock_item1, mock_item2])
] # ]
calculator = CarFinanceCalculator(self.mock_model) # calculator = CarFinanceCalculator(self.mock_model)
totals = calculator.calculate_totals() # totals = calculator.calculate_totals()
expected_total_price = (Decimal("10000") * 2 + Decimal("20000") * 3) - ( # expected_total_price = (Decimal("10000") * 2 + Decimal("20000") * 3) - (
Decimal("500") + Decimal("1000") # Decimal("500") + Decimal("1000")
) # )
expected_vat = expected_total_price * Decimal("0.15") # expected_vat = expected_total_price * Decimal("0.15")
expected_additionals = Decimal("100") + Decimal("200") + Decimal("300") # expected_additionals = Decimal("100") + Decimal("200") + Decimal("300")
expected_grand_total = ( # expected_grand_total = (
expected_total_price + expected_vat + expected_additionals # expected_total_price + expected_vat + expected_additionals
).quantize(Decimal("0.00")) # ).quantize(Decimal("0.00"))
self.assertEqual(totals["total_price"], expected_total_price) # self.assertEqual(totals["total_price"], expected_total_price)
self.assertEqual(totals["total_discount"], Decimal("1500")) # self.assertEqual(totals["total_discount"], Decimal("1500"))
self.assertEqual(totals["total_vat_amount"], expected_vat) # self.assertEqual(totals["total_vat_amount"], expected_vat)
self.assertEqual(totals["total_additionals"], expected_additionals) # self.assertEqual(totals["total_additionals"], expected_additionals)
self.assertEqual(totals["grand_total"], expected_grand_total) # self.assertEqual(totals["grand_total"], expected_grand_total)
def test_get_finance_data(self): # def test_get_finance_data(self):
mock_item = MagicMock() # mock_item = MagicMock()
mock_item.ce_quantity = 1 # mock_item.ce_quantity = 1
mock_item.item_model = MagicMock() # mock_item.item_model = MagicMock()
mock_item.item_model.item_number = "456" # mock_item.item_model.item_number = "456"
mock_item.item_model.additional_info = { # mock_item.item_model.additional_info = {
CarFinanceCalculator.CAR_FINANCE_KEY: { # CarFinanceCalculator.CAR_FINANCE_KEY: {
"selling_price": "15000", # "selling_price": "15000",
"discount_amount": "1000", # "discount_amount": "1000",
"total_vat": "2800", # "total_vat": "2800",
}, # },
CarFinanceCalculator.CAR_INFO_KEY: { # CarFinanceCalculator.CAR_INFO_KEY: {
"vin": "VIN456", # "vin": "VIN456",
"make": "Honda", # "make": "Honda",
"model": "Civic", # "model": "Civic",
"year": 2021, # "year": 2021,
}, # },
CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [ # CarFinanceCalculator.ADDITIONAL_SERVICES_KEY: [
{"name": "Service", "price": "150", "taxable": True, "price_": "180"} # {"name": "Service", "price": "150", "taxable": True, "price_": "180"}
], # ],
} # }
self.mock_model.get_itemtxs_data.return_value = [ # self.mock_model.get_itemtxs_data.return_value = [
MagicMock(all=lambda: [mock_item]) # MagicMock(all=lambda: [mock_item])
] # ]
calculator = CarFinanceCalculator(self.mock_model) # calculator = CarFinanceCalculator(self.mock_model)
finance_data = calculator.get_finance_data() # finance_data = calculator.get_finance_data()
self.assertEqual(len(finance_data["cars"]), 1) # self.assertEqual(len(finance_data["cars"]), 1)
self.assertEqual(finance_data["quantity"], 1) # self.assertEqual(finance_data["quantity"], 1)
self.assertEqual(finance_data["total_price"], Decimal("14000")) # 15000 - 1000 # self.assertEqual(finance_data["total_price"], Decimal("14000")) # 15000 - 1000
self.assertEqual( # self.assertEqual(
finance_data["total_vat"], # finance_data["total_vat"],
Decimal("14000") + (Decimal("14000") * Decimal("0.20")), # Decimal("14000") + (Decimal("14000") * Decimal("0.20")),
) # )
self.assertEqual( # self.assertEqual(
finance_data["total_vat_amount"], Decimal("14000") * Decimal("0.20") # finance_data["total_vat_amount"], Decimal("14000") * Decimal("0.20")
) # )
self.assertEqual(finance_data["total_additionals"], Decimal("180")) # self.assertEqual(finance_data["total_additionals"], Decimal("180"))
self.assertEqual(finance_data["additionals"][0]["name"], "Service") # self.assertEqual(finance_data["additionals"][0]["name"], "Service")
self.assertEqual(finance_data["vat"], Decimal("0.20")) # self.assertEqual(finance_data["vat"], Decimal("0.20"))

View File

@ -1368,14 +1368,11 @@ def create_make_accounts(dealer):
def handle_payment(request, order): def handle_payment(request, order):
url = "https://api.moyasar.com/v1/payments" 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: 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"] email = request.POST["email"]
first_name = request.POST["first_name"] first_name = request.POST["first_name"]
last_name = request.POST["last_name"] last_name = request.POST["last_name"]
@ -1430,12 +1427,13 @@ def handle_payment(request, order):
else: else:
print("Failed to process payment:", data) print("Failed to process payment:", data)
# #
order.status = AbstractOrder.STATUS.NEW data = response.json()
print(data)
# order.status = AbstractOrder.STATUS.NEW
order.save() order.save()
# #
data = response.json()
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100))) amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
print(data)
models.PaymentHistory.objects.create( models.PaymentHistory.objects.create(
user=request.user, user=request.user,
user_data=user_data, user_data=user_data,
@ -1447,7 +1445,6 @@ def handle_payment(request, order):
gateway_response=data, gateway_response=data,
) )
transaction_url = data["source"]["transaction_url"] transaction_url = data["source"]["transaction_url"]
return transaction_url return transaction_url

View File

@ -324,9 +324,7 @@ def dealer_signup(request):
if password != password_confirm: if password != password_confirm:
return JsonResponse({"error": _("Passwords do not match")}, status=400) return JsonResponse({"error": _("Passwords do not match")}, status=400)
try: try:
async_task(create_user_dealer( async_task(create_user_dealer,email, password, name, arabic_name, phone, crn, vrn, address)
email, password, name, arabic_name, phone, crn, vrn, address
))
logger.info(f"Delear created succesfully with emailID {email}") logger.info(f"Delear created succesfully with emailID {email}")
return JsonResponse({"message": _("User created successfully")}, status=200) return JsonResponse({"message": _("User created successfully")}, status=200)
except Exception as e: except Exception as e:
@ -608,7 +606,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) 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) form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
return form return form
@ -4210,15 +4208,13 @@ def sales_list_view(request, dealer_slug):
staff = getattr(request.user.staffmember, "staff", None) staff = getattr(request.user.staffmember, "staff", None)
qs = [] qs = []
try: 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) qs = models.ExtraInfo.get_sale_orders(staff=staff,is_dealer=True)
elif request.is_staff: elif request.is_staff:
qs = models.ExtraInfo.get_sale_orders(staff=staff) qs = models.ExtraInfo.get_sale_orders(staff=staff)
except Exception as e: except Exception as e:
print(e) print(e)
# sale_orders = models.SaleOrder.objects.filter(
# dealer=dealer,
# )
paginator = Paginator(qs, 30) paginator = Paginator(qs, 30)
page_number = request.GET.get("page") page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number) 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"]) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
staff = getattr(self.request.user.staffmember, "staff", None) 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( qs = models.ExtraInfo.objects.filter(
content_type=ContentType.objects.get_for_model(EstimateModel), content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(models.Staff), 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( qs = models.ExtraInfo.objects.filter(
content_type=ContentType.objects.get_for_model(EstimateModel), content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(models.Staff), related_content_type=ContentType.objects.get_for_model(models.Staff),
@ -4860,7 +4856,7 @@ def estimate_mark_as(request, dealer_slug, pk):
estimate.save() estimate.save()
#Reserve The Car #Reserve The Car
car = estimate.get_itemtxs_data()[0].first().item_model.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")) messages.success(request, _("Quotation approved successfully"))
return redirect( return redirect(
"estimate_list", dealer_slug=dealer.slug "estimate_list", dealer_slug=dealer.slug
@ -4939,7 +4935,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
staff = getattr(self.request.user.staffmember, "staff", None) staff = getattr(self.request.user.staffmember, "staff", None)
qs = [] qs = []
try: 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) qs = models.ExtraInfo.get_invoices(staff=staff,is_dealer=True)
elif self.request.is_staff: elif self.request.is_staff:
qs = models.ExtraInfo.get_invoices(staff=staff) qs = models.ExtraInfo.get_invoices(staff=staff)
@ -5587,8 +5583,8 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
qs = apply_search_filters(qs, query) qs = apply_search_filters(qs, query)
if self.request.is_dealer: if self.request.is_dealer:
return qs return qs
staffmember = getattr(self.request.user, "staffmember", None) if self.request.user.is_staff:
if staff := getattr(staffmember, "staff", None): staff = getattr(self.request.user.staffmember, "staff", None)
return qs.filter(staff=staff) return qs.filter(staff=staff)
return models.Lead.objects.none() return models.Lead.objects.none()
@ -7540,7 +7536,6 @@ class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
def get_queryset(self): def get_queryset(self):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
qs = super().get_queryset() qs = super().get_queryset()
print(qs)
return qs.filter(estimate__entity=dealer.entity) 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) dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
selected_plan_id = request.POST.get("selected_plan") selected_plan_id = request.POST.get("selected_plan")
pp = PlanPricing.objects.get(pk=selected_plan_id) pp = PlanPricing.objects.get(pk=selected_plan_id)
order = None
order = Order.objects.create( try:
user=dealer.user, order = Order.objects.create(
plan=pp.plan, user=dealer.user,
pricing=pp.pricing, plan=pp.plan,
amount=pp.price, pricing=pp.pricing,
currency=settings.DEFAULT_CURRENCY, amount=pp.price,
tax=15, currency="SA",
status=AbstractOrder.STATUS.NEW, 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) transaction_url = handle_payment(request, order)
return redirect(transaction_url) return redirect(transaction_url)
@ -9130,7 +9131,7 @@ def payment_callback(request,dealer_slug):
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
payment_status = request.GET.get("status") payment_status = request.GET.get("status")
order = Order.objects.filter( order = Order.objects.filter(
user=dealer.user, status=AbstractOrder.STATUS.NEW user=dealer.user, status=1
).first() ).first()
if payment_status == "paid": if payment_status == "paid":
@ -9818,6 +9819,7 @@ def InventoryItemCreateView(request, dealer_slug):
# return redirect("purchase_order_list", dealer_slug=dealer.slug) # return redirect("purchase_order_list", dealer_slug=dealer.slug)
if for_po: if for_po:
form = forms.CSVUploadForm() 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) form.fields["vendor"].queryset = dealer.vendors.filter(active=True)
context = { context = {
"make_data": models.CarMake.objects.filter(is_sa_import=True), "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) @permission_required("django_ledger.view_purchaseordermodel", raise_exception=True)
def inventory_items_filter(request, dealer_slug): def inventory_items_filter(request, dealer_slug):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
year = request.GET.get("year", None)
make = request.GET.get("make") make = request.GET.get("make")
model = request.GET.get("model") model = request.GET.get("model")
serie = request.GET.get("serie") serie = request.GET.get("serie")
@ -9853,6 +9856,9 @@ def inventory_items_filter(request, dealer_slug):
elif model: elif model:
model = models.CarModel.objects.get(pk=model) model = models.CarModel.objects.get(pk=model)
serie_data = model.carserie_set.all() 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: elif serie:
serie = models.CarSerie.objects.get(pk=serie) serie = models.CarSerie.objects.get(pk=serie)
trim_data = serie.cartrim_set.all() trim_data = serie.cartrim_set.all()
@ -9860,8 +9866,6 @@ def inventory_items_filter(request, dealer_slug):
"model_data": model_data, "model_data": model_data,
"serie_data": serie_data, "serie_data": serie_data,
"trim_data": trim_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) return render(request, "purchase_orders/car_inventory_item_form.html", context)

View File

@ -109,3 +109,25 @@ html[dir="rtl"] .form-icon-container .form-control {
padding-left: 10px; 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); }
}

View File

@ -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) { function getCookie(name) {
let cookieValue = null; let cookieValue = null;
@ -222,3 +248,4 @@ const getDataTableInit = () => {
} }
}; };

View File

@ -1121,6 +1121,7 @@ a.deletelink:focus, a.deletelink:hover {
margin: 0; margin: 0;
border-top: 1px solid var(--hairline-color); border-top: 1px solid var(--hairline-color);
width: 100%; width: 100%;
box-sizing: border-box;
} }
.paginator a:link, .paginator a:visited { .paginator a:link, .paginator a:visited {

View File

@ -84,8 +84,8 @@ html[data-theme="dark"] {
.theme-toggle svg { .theme-toggle svg {
vertical-align: middle; vertical-align: middle;
height: 1rem; height: 1.5rem;
width: 1rem; width: 1.5rem;
display: none; display: none;
} }

View File

@ -449,17 +449,6 @@ body.popup .submit-row {
_width: 700px; _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 div.add-row,
.inline-group .tabular tr.add-row td { .inline-group .tabular tr.add-row td {
color: var(--body-quiet-color); color: var(--body-quiet-color);
@ -473,11 +462,8 @@ body.popup .submit-row {
border-bottom: 1px solid var(--hairline-color); border-bottom: 1px solid var(--hairline-color);
} }
.inline-group ul.tools a.add,
.inline-group div.add-row a, .inline-group div.add-row a,
.inline-group .tabular tr.add-row td 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; font-size: 0.75rem;
} }

View File

@ -254,10 +254,6 @@ input[type="submit"], button {
align-items: center; align-items: center;
} }
.selector .selector-filter label {
margin: 0 8px 0 0;
}
.selector .selector-filter input { .selector .selector-filter input {
width: 100%; width: 100%;
min-height: 0; min-height: 0;
@ -277,29 +273,7 @@ input[type="submit"], button {
margin-bottom: 5px; margin-bottom: 5px;
} }
.selector ul.selector-chooser { .selector-chooseall, .selector-clearall {
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; align-self: center;
} }
@ -321,8 +295,6 @@ input[type="submit"], button {
} }
.stacked ul.selector-chooser { .stacked ul.selector-chooser {
width: 52px;
height: 26px;
padding: 0 2px; padding: 0 2px;
transform: none; transform: none;
} }
@ -331,42 +303,6 @@ input[type="submit"], button {
padding: 3px; 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 { .help-tooltip, .selector .help-icon {
display: none; display: none;
} }
@ -649,6 +585,7 @@ input[type="submit"], button {
.related-widget-wrapper .selector { .related-widget-wrapper .selector {
order: 1; order: 1;
flex: 1 0 auto;
} }
.related-widget-wrapper > a { .related-widget-wrapper > a {
@ -679,9 +616,9 @@ input[type="submit"], button {
} }
.selector ul.selector-chooser { .selector ul.selector-chooser {
display: block; display: flex;
width: 52px; width: 60px;
height: 26px; height: 30px;
padding: 0 2px; padding: 0 2px;
transform: none; transform: none;
} }
@ -694,16 +631,16 @@ input[type="submit"], button {
background-position: 0 0; background-position: 0 0;
} }
.active.selector-remove:focus, .active.selector-remove:hover { :enabled.selector-remove:focus, :enabled.selector-remove:hover {
background-position: 0 -20px; background-position: 0 -24px;
} }
.selector-add { .selector-add {
background-position: 0 -40px; background-position: 0 -48px;
} }
.active.selector-add:focus, .active.selector-add:hover { :enabled.selector-add:focus, :enabled.selector-add:hover {
background-position: 0 -60px; background-position: 0 -72px;
} }
/* Inlines */ /* Inlines */

View File

@ -28,18 +28,12 @@
margin-left: 0; margin-left: 0;
} }
[dir="rtl"] .inline-group ul.tools a.add,
[dir="rtl"] .inline-group div.add-row a, [dir="rtl"] .inline-group div.add-row a,
[dir="rtl"] .inline-group .tabular tr.add-row td a { [dir="rtl"] .inline-group .tabular tr.add-row td a {
padding: 8px 26px 8px 10px; padding: 8px 26px 8px 10px;
background-position: calc(100% - 8px) 9px; background-position: calc(100% - 8px) 9px;
} }
[dir="rtl"] .selector .selector-filter label {
margin-right: 0;
margin-left: 8px;
}
[dir="rtl"] .object-tools li { [dir="rtl"] .object-tools li {
float: right; float: right;
} }
@ -53,22 +47,6 @@
padding-left: 0; padding-left: 0;
padding-right: 16px; 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 */ /* MOBILE */
@ -97,15 +75,15 @@
background-position: 0 0; background-position: 0 0;
} }
[dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover { [dir="rtl"] :enabled.selector-remove:focus, :enabled.selector-remove:hover {
background-position: 0 -20px; background-position: 0 -24px;
} }
[dir="rtl"] .selector-add { [dir="rtl"] .selector-add {
background-position: 0 -40px; background-position: 0 -48px;
} }
[dir="rtl"] .active.selector-add:focus, .active.selector-add:hover { [dir="rtl"] :enabled.selector-add:focus, :enabled.selector-add:hover {
background-position: 0 -60px; background-position: 0 -72px;
} }
} }

View File

@ -220,34 +220,36 @@ fieldset .fieldBox {
} }
.selector-add { .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 { :enabled.selector-add:focus, :enabled.selector-add:hover {
background-position: 0 -80px; background-position: 0 -120px;
} }
.selector-remove { .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 { :enabled.selector-remove:focus, :enabled.selector-remove:hover {
background-position: 0 -112px; background-position: 0 -168px;
} }
a.selector-chooseall { .selector-chooseall {
background: url(../img/selector-icons.svg) right -128px no-repeat; 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; background-position: 100% -144px;
} }
a.selector-clearall { .selector-clearall {
background: url(../img/selector-icons.svg) 0 -160px no-repeat; 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; background-position: 0 -176px;
} }

View File

@ -2,7 +2,7 @@
.selector { .selector {
display: flex; display: flex;
flex-grow: 1; flex: 1;
gap: 0 10px; gap: 0 10px;
} }
@ -14,17 +14,20 @@
} }
.selector-available, .selector-chosen { .selector-available, .selector-chosen {
text-align: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1; flex: 1 1;
} }
.selector-available h2, .selector-chosen h2 { .selector-available-title, .selector-chosen-title {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
} }
.selector .helptext {
font-size: 0.6875rem;
}
.selector-chosen .list-footer-display { .selector-chosen .list-footer-display {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-top: none; border-top: none;
@ -40,14 +43,25 @@
color: var(--breadcrumbs-fg); color: var(--breadcrumbs-fg);
} }
.selector-chosen h2 { .selector-chosen-title {
background: var(--secondary); background: var(--secondary);
color: var(--header-link-color); 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); background: var(--darkened-bg);
color: var(--body-quiet-color); color: var(--body-quiet-color);
padding: 8px;
}
.aligned .selector-available-title label {
width: 100%;
} }
.selector .selector-filter { .selector .selector-filter {
@ -59,6 +73,7 @@
margin: 0; margin: 0;
text-align: left; text-align: left;
display: flex; display: flex;
gap: 8px;
} }
.selector .selector-filter label, .selector .selector-filter label,
@ -77,14 +92,9 @@
flex-grow: 1; flex-grow: 1;
} }
.selector .selector-available input,
.selector .selector-chosen input {
margin-left: 8px;
}
.selector ul.selector-chooser { .selector ul.selector-chooser {
align-self: center; align-self: center;
width: 22px; width: 30px;
background-color: var(--selected-bg); background-color: var(--selected-bg);
border-radius: 10px; border-radius: 10px;
margin: 0; margin: 0;
@ -114,40 +124,43 @@
} }
.selector-add, .selector-remove { .selector-add, .selector-remove {
width: 16px; width: 24px;
height: 16px; height: 24px;
display: block; display: block;
text-indent: -3000px; text-indent: -3000px;
overflow: hidden; overflow: hidden;
cursor: default; cursor: default;
opacity: 0.55; opacity: 0.55;
border: none;
} }
.active.selector-add, .active.selector-remove { :enabled.selector-add, :enabled.selector-remove {
opacity: 1; opacity: 1;
} }
.active.selector-add:hover, .active.selector-remove:hover { :enabled.selector-add:hover, :enabled.selector-remove:hover {
cursor: pointer; cursor: pointer;
} }
.selector-add { .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 { :enabled.selector-add:focus, :enabled.selector-add:hover {
background-position: 0 -112px; background-position: 0 -168px;
} }
.selector-remove { .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 { :enabled.selector-remove:focus, :enabled.selector-remove:hover {
background-position: 0 -80px; background-position: 0 -120px;
} }
a.selector-chooseall, a.selector-clearall { .selector-chooseall, .selector-clearall {
display: inline-block; display: inline-block;
height: 16px; height: 16px;
text-align: left; text-align: left;
@ -158,38 +171,39 @@ a.selector-chooseall, a.selector-clearall {
color: var(--body-quiet-color); color: var(--body-quiet-color);
text-decoration: none; text-decoration: none;
opacity: 0.55; opacity: 0.55;
border: none;
} }
a.active.selector-chooseall:focus, a.active.selector-clearall:focus, :enabled.selector-chooseall:focus, :enabled.selector-clearall:focus,
a.active.selector-chooseall:hover, a.active.selector-clearall:hover { :enabled.selector-chooseall:hover, :enabled.selector-clearall:hover {
color: var(--link-fg); color: var(--link-fg);
} }
a.active.selector-chooseall, a.active.selector-clearall { :enabled.selector-chooseall, :enabled.selector-clearall {
opacity: 1; opacity: 1;
} }
a.active.selector-chooseall:hover, a.active.selector-clearall:hover { :enabled.selector-chooseall:hover, :enabled.selector-clearall:hover {
cursor: pointer; cursor: pointer;
} }
a.selector-chooseall { .selector-chooseall {
padding: 0 18px 0 0; padding: 0 18px 0 0;
background: url(../img/selector-icons.svg) right -160px no-repeat; background: url(../img/selector-icons.svg) right -160px no-repeat;
cursor: default; cursor: default;
} }
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover { :enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover {
background-position: 100% -176px; background-position: 100% -176px;
} }
a.selector-clearall { .selector-clearall {
padding: 0 0 0 18px; padding: 0 0 0 18px;
background: url(../img/selector-icons.svg) 0 -128px no-repeat; background: url(../img/selector-icons.svg) 0 -128px no-repeat;
cursor: default; cursor: default;
} }
a.active.selector-clearall:focus, a.active.selector-clearall:hover { :enabled.selector-clearall:focus, :enabled.selector-clearall:hover {
background-position: 0 -144px; background-position: 0 -144px;
} }
@ -219,8 +233,9 @@ a.active.selector-clearall:focus, a.active.selector-clearall:hover {
} }
.stacked ul.selector-chooser { .stacked ul.selector-chooser {
height: 22px; display: flex;
width: 50px; height: 30px;
width: 64px;
margin: 0 0 10px 40%; margin: 0 0 10px 40%;
background-color: #eee; background-color: #eee;
border-radius: 10px; border-radius: 10px;
@ -237,32 +252,34 @@ a.active.selector-clearall:focus, a.active.selector-clearall:hover {
} }
.stacked .selector-add { .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; cursor: default;
} }
.stacked .active.selector-add { .stacked :enabled.selector-add {
background-position: 0 -32px; background-position: 0 -48px;
cursor: pointer; cursor: pointer;
} }
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover { .stacked :enabled.selector-add:focus, .stacked :enabled.selector-add:hover {
background-position: 0 -48px; background-position: 0 -72px;
cursor: pointer; cursor: pointer;
} }
.stacked .selector-remove { .stacked .selector-remove {
background: url(../img/selector-icons.svg) 0 0 no-repeat; background: url(../img/selector-icons.svg) 0 0 no-repeat;
background-size: 24px auto;
cursor: default; cursor: default;
} }
.stacked .active.selector-remove { .stacked :enabled.selector-remove {
background-position: 0 0px; background-position: 0 0px;
cursor: pointer; cursor: pointer;
} }
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover { .stacked :enabled.selector-remove:focus, .stacked :enabled.selector-remove:hover {
background-position: 0 -16px; background-position: 0 -24px;
cursor: pointer; cursor: pointer;
} }
@ -318,28 +335,30 @@ table p.datetime {
position: relative; position: relative;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
height: 16px; height: 24px;
width: 16px; width: 24px;
overflow: hidden; overflow: hidden;
} }
.datetimeshortcuts .clock-icon { .datetimeshortcuts .clock-icon {
background: url(../img/icon-clock.svg) 0 0 no-repeat; background: url(../img/icon-clock.svg) 0 0 no-repeat;
background-size: 24px auto;
} }
.datetimeshortcuts a:focus .clock-icon, .datetimeshortcuts a:focus .clock-icon,
.datetimeshortcuts a:hover .clock-icon { .datetimeshortcuts a:hover .clock-icon {
background-position: 0 -16px; background-position: 0 -24px;
} }
.datetimeshortcuts .date-icon { .datetimeshortcuts .date-icon {
background: url(../img/icon-calendar.svg) 0 0 no-repeat; background: url(../img/icon-calendar.svg) 0 0 no-repeat;
background-size: 24px auto;
top: -1px; top: -1px;
} }
.datetimeshortcuts a:focus .date-icon, .datetimeshortcuts a:focus .date-icon,
.datetimeshortcuts a:hover .date-icon { .datetimeshortcuts a:hover .date-icon {
background-position: 0 -16px; background-position: 0 -24px;
} }
.timezonewarning { .timezonewarning {
@ -558,9 +577,10 @@ ul.timelist, .timelist li {
float: right; float: right;
text-indent: -9999px; text-indent: -9999px;
background: url(../img/inline-delete.svg) 0 0 no-repeat; background: url(../img/inline-delete.svg) 0 0 no-repeat;
width: 16px; width: 1.5rem;
height: 16px; height: 1.5rem;
border: 0px none; border: 0px none;
margin-bottom: .25rem;
} }
.inline-deletelink:focus, .inline-deletelink:hover { .inline-deletelink:focus, .inline-deletelink:hover {

View File

@ -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"/> <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> </svg>

Before

Width:  |  Height:  |  Size: 560 B

After

Width:  |  Height:  |  Size: 537 B

View File

@ -15,6 +15,7 @@ Requires core.js and SelectBox.js.
const from_box = document.getElementById(field_id); const from_box = document.getElementById(field_id);
from_box.id += '_from'; // change its ID from_box.id += '_from'; // change its ID
from_box.className = 'filtered'; from_box.className = 'filtered';
from_box.setAttribute('aria-labelledby', field_id + '_from_title');
for (const p of from_box.parentNode.getElementsByTagName('p')) { for (const p of from_box.parentNode.getElementsByTagName('p')) {
if (p.classList.contains("info")) { if (p.classList.contains("info")) {
@ -38,18 +39,15 @@ Requires core.js and SelectBox.js.
// <div class="selector-available"> // <div class="selector-available">
const selector_available = quickElement('div', selector_div); const selector_available = quickElement('div', selector_div);
selector_available.className = 'selector-available'; 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( quickElement(
'span', title_available, '', 'p',
'class', 'help help-tooltip help-icon', selector_available_title,
'title', interpolate( interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
gettext( 'class', 'helptext'
'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]
)
); );
const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter'); const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
@ -60,7 +58,7 @@ Requires core.js and SelectBox.js.
quickElement( quickElement(
'span', search_filter_label, '', 'span', search_filter_label, '',
'class', 'help-tooltip search-label-icon', '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(' ')); filter_p.appendChild(document.createTextNode(' '));
@ -69,32 +67,47 @@ Requires core.js and SelectBox.js.
filter_input.id = field_id + '_input'; filter_input.id = field_id + '_input';
selector_available.appendChild(from_box); 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'); const choose_all = quickElement(
choose_all.className = 'selector-chooseall'; 'button',
selector_available,
interpolate(gettext('Choose all %s'), [field_name]),
'id', field_id + '_add_all',
'class', 'selector-chooseall',
'type', 'button'
);
// <ul class="selector-chooser"> // <ul class="selector-chooser">
const selector_chooser = quickElement('ul', selector_div); const selector_chooser = quickElement('ul', selector_div);
selector_chooser.className = 'selector-chooser'; 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'); const add_button = quickElement(
add_link.className = 'selector-add'; 'button',
const remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link'); quickElement('li', selector_chooser),
remove_link.className = 'selector-remove'; 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"> // <div class="selector-chosen">
const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen'); const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen');
selector_chosen.className = '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( quickElement(
'span', title_chosen, '', 'p',
'class', 'help help-tooltip help-icon', selector_chosen_title,
'title', interpolate( interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
gettext( 'class', 'helptext'
'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]
)
); );
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected'); const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
@ -105,7 +118,7 @@ Requires core.js and SelectBox.js.
quickElement( quickElement(
'span', search_filter_selected_label, '', 'span', search_filter_selected_label, '',
'class', 'help-tooltip search-label-icon', '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(' ')); 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")); const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
filter_selected_input.id = field_id + '_selected_input'; 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); quickElement(
to_box.className = 'filtered'; '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'); 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, '', 'id', field_id + '_list-footer-display-text');
quickElement('span', warning_footer, ' (click to clear)', 'class', 'list-footer-display__clear'); quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear');
const clear_all = quickElement(
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'); 'button',
clear_all.className = 'selector-clearall'; 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'; from_box.name = from_box.name + '_old';
// Set up the JavaScript event handlers for the select box filter interface // Set up the JavaScript event handlers for the select box filter interface
const move_selection = function(e, elem, move_func, from, to) { const move_selection = function(e, elem, move_func, from, to) {
if (elem.classList.contains('active')) { if (!elem.hasAttribute('disabled')) {
move_func(from, to); move_func(from, to);
SelectFilter.refresh_icons(field_id); SelectFilter.refresh_icons(field_id);
SelectFilter.refresh_filtered_selects(field_id); SelectFilter.refresh_filtered_selects(field_id);
@ -138,10 +164,10 @@ Requires core.js and SelectBox.js.
choose_all.addEventListener('click', function(e) { choose_all.addEventListener('click', function(e) {
move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to'); 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'); 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'); move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
}); });
clear_all.addEventListener('click', function(e) { clear_all.addEventListener('click', function(e) {
@ -226,13 +252,12 @@ Requires core.js and SelectBox.js.
refresh_icons: function(field_id) { refresh_icons: function(field_id) {
const from = document.getElementById(field_id + '_from'); const from = document.getElementById(field_id + '_from');
const to = document.getElementById(field_id + '_to'); const to = document.getElementById(field_id + '_to');
// Active if at least one item is selected // Disabled if no items are selected.
document.getElementById(field_id + '_add_link').classList.toggle('active', SelectFilter.any_selected(from)); document.getElementById(field_id + '_add').disabled = !SelectFilter.any_selected(from);
document.getElementById(field_id + '_remove_link').classList.toggle('active', SelectFilter.any_selected(to)); document.getElementById(field_id + '_remove').disabled = !SelectFilter.any_selected(to);
// Active if the corresponding box isn't empty // Disabled if the corresponding box is empty.
document.getElementById(field_id + '_add_all_link').classList.toggle('active', from.querySelector('option')); document.getElementById(field_id + '_add_all').disabled = !from.querySelector('option');
document.getElementById(field_id + '_remove_all_link').classList.toggle('active', to.querySelector('option')); document.getElementById(field_id + '_remove_all').disabled = !to.querySelector('option');
SelectFilter.refresh_filtered_warning(field_id);
}, },
filter_key_press: function(event, field_id, source, target) { filter_key_press: function(event, field_id, source, target) {
const source_box = document.getElementById(field_id + source); const source_box = document.getElementById(field_id + source);

View File

@ -55,8 +55,9 @@
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) { if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
elem.value += ',' + chosenId; elem.value += ',' + chosenId;
} else { } else {
document.getElementById(name).value = chosenId; elem.value = chosenId;
} }
$(elem).trigger('change');
const index = relatedWindows.indexOf(win); const index = relatedWindows.indexOf(win);
if (index > -1) { if (index > -1) {
relatedWindows.splice(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 // After create/edit a model from the options next to the current
// select (+ or :pencil:) update ForeignKey PK of the rest of selects // select (+ or :pencil:) update ForeignKey PK of the rest of selects
// in the page. // in the page.
@ -100,7 +101,7 @@
const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`); const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`);
selectsRelated.forEach(function(select) { selectsRelated.forEach(function(select) {
if (currentSelect === select) { if (currentSelect === select || skipIds && skipIds.includes(select.id)) {
return; return;
} }
@ -109,6 +110,11 @@
if (!option) { if (!option) {
option = new Option(newRepr, newId); option = new Option(newRepr, newId);
select.options.add(option); 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; return;
} }
@ -136,9 +142,14 @@
$(elem).trigger('change'); $(elem).trigger('change');
} else { } else {
const toId = name + "_to"; const toId = name + "_to";
const toElem = document.getElementById(toId);
const o = new Option(newRepr, newId); const o = new Option(newRepr, newId);
SelectBox.add_to_cache(toId, o); SelectBox.add_to_cache(toId, o);
SelectBox.redisplay(toId); 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); const index = relatedWindows.indexOf(win);
if (index > -1) { if (index > -1) {
@ -195,6 +206,7 @@
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup; window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup; window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
window.dismissChildPopups = dismissChildPopups; window.dismissChildPopups = dismissChildPopups;
window.relatedWindows = relatedWindows;
// Kept for backward compatibility // Kept for backward compatibility
window.showAddAnotherPopup = showRelatedObjectPopup; window.showAddAnotherPopup = showRelatedObjectPopup;

View File

@ -50,11 +50,11 @@
// If forms are laid out as table rows, insert the // If forms are laid out as table rows, insert the
// "add" button in a new table row: // "add" button in a new table row:
const numCols = $this.eq(-1).children().length; 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"); addButton = $parent.find("tr:last a");
} else { } else {
// Otherwise, insert it immediately after the last form: // 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"); addButton = $this.filter(":last").next().find("a");
} }
} }
@ -104,15 +104,15 @@
if (row.is("tr")) { if (row.is("tr")) {
// If the forms are laid out in table rows, insert // If the forms are laid out in table rows, insert
// the remove button into the last table cell: // 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")) { } else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list, // If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item: // 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 { } else {
// Otherwise, just insert the remove button as the // Otherwise, just insert the remove button as the
// last child element of the form's container: // 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. // Add delete handler for each row.
row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this)); row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this));

View File

@ -109,3 +109,25 @@ html[dir="rtl"] .form-icon-container .form-control {
padding-left: 10px; 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); }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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) { function getCookie(name) {
let cookieValue = null; let cookieValue = null;
@ -222,3 +248,4 @@ const getDataTableInit = () => {
} }
}; };

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
{% block content %} {% block content %}
<div class="container py-4"> <div class="container py-4">
<div class="row g-2"> <div class="row g-2">
<!-- Bill Form --> <!-- Bill Form -->
<div class="col-12"> <div class="col-12">
@ -18,25 +18,38 @@
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <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 %} {% 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 %}" <form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
class="btn btn-phoenix-secondary w-100 mb-2"> {% csrf_token %}
<i class="fas fa-arrow-left me-2"></i>{% trans 'Back to Bill Detail' %}
</a> <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> </div>
</div> </div>
</div> </div>
<!-- Bill Item Formset --> <!-- Bill Item Formset -->
<div class="col-12"> <div class="col-12">
{% bill_item_formset_table itemtxs_formset %} {% bill_item_formset_table itemtxs_formset %}

View File

@ -101,7 +101,7 @@
<!-- Total Amount --> <!-- Total Amount -->
<td class="text-end"> <td class="text-end">
<span class="text-xs font-weight-bold"> <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> </span>
</td> </td>

View File

@ -78,6 +78,10 @@
<div class="col"> <div class="col">
{% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %} {% include "purchase_orders/partials/po-select.html" with name="model" target="serie" data=model_data pk=po_model.pk %}
</div> </div>
<div class="col">
{{form.year.label}}
{{form.year}}
</div>
<div class="col"> <div class="col">
{% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %} {% include "purchase_orders/partials/po-select.html" with name="serie" target="trim" data=serie_data pk=po_model.pk %}
</div> </div>
@ -90,10 +94,6 @@
{{form.vendor.label}} {{form.vendor.label}}
{{form.vendor}} {{form.vendor}}
</div> </div>
<div class="col">
{{form.year.label}}
{{form.year}}
</div>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -7,8 +7,10 @@
hx-target="#form-{{target}}" hx-target="#form-{{target}}"
hx-select="#form-{{target}}" hx-select="#form-{{target}}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-include="#id_year"
{% endif %} {% endif %}
> >
<option value="">--------</option>
{% for item in data %} {% for item in data %}
<option value="{{ item.pk }}">{{ item.name }}</option> <option value="{{ item.pk }}">{{ item.name }}</option>
{% endfor %} {% endfor %}

View File

@ -82,7 +82,7 @@
</div> </div>
</div> </div>
<div class="d-flex align-items-center gap-2"> <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' %} {% 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> <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 %} {% endif %}
@ -98,7 +98,7 @@
{% endif %} {% endif %}
{% 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> <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>
</div> </div>
{{invoice.amount_owned}} {{invoice.amount_owned}}

View File

@ -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">{{ user.email }}</td>
<td class="align-middle white-space-nowrap align-items-center justify-content-center">{{ user.phone_number }}</td> <td class="align-middle white-space-nowrap align-items-center justify-content-center">{{ user.phone_number }}</td>
<td> <td>
{% for group in user.groups %} {% 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> <span class="badge badge-sm bg-primary text-center"><i class="fa-solid fa-scroll"></i> {% trans group.name|title %}</span>
{% endfor %} {% endfor %}