fix the value too long issue in django plan
This commit is contained in:
parent
4b7bf44923
commit
5884a0cb8d
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.7 on 2025-07-01 10:33
|
# Generated by Django 5.2.4 on 2025-07-09 13:00
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.7 on 2025-07-01 10:33
|
# Generated by Django 5.2.4 on 2025-07-09 13:00
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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=[
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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); }
|
||||||
|
}
|
||||||
@ -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 = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 |
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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); }
|
||||||
|
}
|
||||||
BIN
staticfiles/images/customers/sun_mountain.jpg
Normal file
BIN
staticfiles/images/customers/sun_mountain.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@ -1,3 +1,29 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Find all forms with class 'disable-on-submit' or target a specific form
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
// Find the submit button within this form
|
||||||
|
const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
|
||||||
|
|
||||||
|
if (submitButton) {
|
||||||
|
// Disable the button
|
||||||
|
submitButton.disabled = true;
|
||||||
|
|
||||||
|
// Optional: Add a loading class for styling
|
||||||
|
submitButton.classList.add('loading');
|
||||||
|
|
||||||
|
// Re-enable the button if the form submission fails
|
||||||
|
// This ensures the button doesn't stay disabled if there's an error
|
||||||
|
window.addEventListener('unload', function() {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.classList.remove('loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
let cookieValue = null;
|
let cookieValue = null;
|
||||||
@ -222,3 +248,4 @@ const getDataTableInit = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
439
staticfiles/schema_graph/main.js
Normal file
439
staticfiles/schema_graph/main.js
Normal file
File diff suppressed because one or more lines are too long
@ -8,7 +8,7 @@
|
|||||||
{% block content %}
|
{% 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 %}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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}}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user