diff --git a/haikalbot/migrations/0001_initial.py b/haikalbot/migrations/0001_initial.py index 31da4e22..4d92b23a 100644 --- a/haikalbot/migrations/0001_initial.py +++ b/haikalbot/migrations/0001_initial.py @@ -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 diff --git a/haikalbot/migrations/0002_initial.py b/haikalbot/migrations/0002_initial.py index 32465ec6..1b740d02 100644 --- a/haikalbot/migrations/0002_initial.py +++ b/haikalbot/migrations/0002_initial.py @@ -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 diff --git a/inventory/forms.py b/inventory/forms.py index 4cb1362f..bc5c4c85 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -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( diff --git a/inventory/middleware.py b/inventory/middleware.py index 992d7069..73ed6a87 100644 --- a/inventory/middleware.py +++ b/inventory/middleware.py @@ -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) diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py index fd3fd633..8e326055 100644 --- a/inventory/migrations/0001_initial.py +++ b/inventory/migrations/0001_initial.py @@ -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=[ diff --git a/inventory/models.py b/inventory/models.py index 4f20430a..ea6b94bc 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -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 diff --git a/inventory/signals.py b/inventory/signals.py index 52b88ed4..3670ba22 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -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() diff --git a/inventory/tasks.py b/inventory/tasks.py index 26667b61..4ae0db3b 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -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) + diff --git a/inventory/templatetags/custom_filters.py b/inventory/templatetags/custom_filters.py index 5b449803..53b75cc8 100644 --- a/inventory/templatetags/custom_filters.py +++ b/inventory/templatetags/custom_filters.py @@ -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"], diff --git a/inventory/tests.py b/inventory/tests.py index 3019e8a3..80cc1e48 100644 --- a/inventory/tests.py +++ b/inventory/tests.py @@ -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")) diff --git a/inventory/utils.py b/inventory/utils.py index 938e0d33..b2130b04 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -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 diff --git a/inventory/views.py b/inventory/views.py index 778fa51f..f9cb2817 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -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) diff --git a/static/css/custom.css b/static/css/custom.css index 80ecd633..fd4b7369 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -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); } +} \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index fcb43e54..da998095 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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 = () => { } }; + diff --git a/staticfiles/admin/css/base.css b/staticfiles/admin/css/base.css index ac283260..37910431 100644 --- a/staticfiles/admin/css/base.css +++ b/staticfiles/admin/css/base.css @@ -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 { diff --git a/staticfiles/admin/css/dark_mode.css b/staticfiles/admin/css/dark_mode.css index 7e12a815..65b58d03 100644 --- a/staticfiles/admin/css/dark_mode.css +++ b/staticfiles/admin/css/dark_mode.css @@ -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; } diff --git a/staticfiles/admin/css/forms.css b/staticfiles/admin/css/forms.css index 4f49b613..c6ce7883 100644 --- a/staticfiles/admin/css/forms.css +++ b/staticfiles/admin/css/forms.css @@ -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; } diff --git a/staticfiles/admin/css/responsive.css b/staticfiles/admin/css/responsive.css index 932e824c..f0fcade4 100644 --- a/staticfiles/admin/css/responsive.css +++ b/staticfiles/admin/css/responsive.css @@ -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 */ diff --git a/staticfiles/admin/css/responsive_rtl.css b/staticfiles/admin/css/responsive_rtl.css index 33b57848..5e8f5c59 100644 --- a/staticfiles/admin/css/responsive_rtl.css +++ b/staticfiles/admin/css/responsive_rtl.css @@ -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; } } diff --git a/staticfiles/admin/css/rtl.css b/staticfiles/admin/css/rtl.css index b8f60e0a..a2556d04 100644 --- a/staticfiles/admin/css/rtl.css +++ b/staticfiles/admin/css/rtl.css @@ -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; } diff --git a/staticfiles/admin/css/widgets.css b/staticfiles/admin/css/widgets.css index cc64811a..538af2eb 100644 --- a/staticfiles/admin/css/widgets.css +++ b/staticfiles/admin/css/widgets.css @@ -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 { diff --git a/staticfiles/admin/img/inline-delete.svg b/staticfiles/admin/img/inline-delete.svg index 17d1ad67..87511505 100644 --- a/staticfiles/admin/img/inline-delete.svg +++ b/staticfiles/admin/img/inline-delete.svg @@ -1,3 +1,3 @@ - + diff --git a/staticfiles/admin/js/SelectFilter2.js b/staticfiles/admin/js/SelectFilter2.js index 69574124..970b511b 100644 --- a/staticfiles/admin/js/SelectFilter2.js +++ b/staticfiles/admin/js/SelectFilter2.js @@ -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. //
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' + ); //