fix the value too long issue in django plan

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

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.7 on 2025-07-01 10:33
# Generated by Django 5.2.4 on 2025-07-09 13:00
import django.db.models.deletion
import django.utils.timezone

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.7 on 2025-07-01 10:33
# Generated by Django 5.2.4 on 2025-07-09 13:00
import django.db.models.deletion
from django.db import migrations, models

View File

@ -2035,7 +2035,17 @@ class CSVUploadForm(forms.Form):
)
year = forms.IntegerField(
label=_("Year"),
widget=forms.NumberInput(attrs={"class": "form-control"}),
widget=forms.NumberInput(attrs=
{
"class": "form-control",
"hx-get": "",
"hx-target": "#serie",
"hx-select": "#serie",
"hx-include": "#model",
"hx-trigger": "input delay:500ms",
"hx-swap": "outerHTML",
}
),
required=True,
)
exterior = forms.ModelChoiceField(

View File

@ -96,10 +96,23 @@ class InjectDealerMiddleware:
if request.user.is_authenticated:
request.is_dealer = False
request.is_staff = False
request.is_manager = False
request.is_accountant = False
request.is_sales = False
request.is_inventory = False
if hasattr(request.user, "dealer"):
request.is_dealer = True
elif hasattr(request.user, "staffmember"):
request.is_staff = True
staff = getattr(request.user.staffmember, "staff")
if "Accountant" in staff.groups.values_list("name", flat=True):
request.is_accountant = True
if "Manager" in staff.groups.values_list("name", flat=True):
request.is_manager = True
if "Sales" in staff.groups.values_list("name", flat=True):
request.is_sales = True
if "Inventory" in staff.groups.values_list("name", flat=True):
request.is_inventory = True
except Exception:
pass
response = self.get_response(request)

View File

@ -1,6 +1,7 @@
# Generated by Django 5.1.7 on 2025-07-01 10:33
# Generated by Django 5.2.4 on 2025-07-09 13:00
import datetime
import django.core.serializers.json
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
@ -21,7 +22,7 @@ class Migration(migrations.Migration):
('appointment', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
('django_ledger', '0023_merge_20250708_1825'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
@ -84,20 +85,6 @@ class Migration(migrations.Migration):
},
bases=(models.Model, inventory.mixins.LocalizedNameMixin),
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
],
options={
'verbose_name': 'payment',
'verbose_name_plural': 'payments',
},
),
migrations.CreateModel(
name='VatRate',
fields=[
@ -600,6 +587,21 @@ class Migration(migrations.Migration):
name='organization',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_leads', to='inventory.organization'),
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')),
('payment_method', models.CharField(choices=[('cash', 'cash'), ('credit', 'credit'), ('transfer', 'transfer'), ('debit', 'debit'), ('sadad', 'SADAD')], max_length=50, verbose_name='method')),
('reference_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='reference number')),
('payment_date', models.DateField(auto_now_add=True, verbose_name='date')),
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='django_ledger.invoicemodel', verbose_name='invoice')),
],
options={
'verbose_name': 'payment',
'verbose_name_plural': 'payments',
},
),
migrations.CreateModel(
name='PoItemsUploaded',
fields=[
@ -677,16 +679,19 @@ class Migration(migrations.Migration):
name='Schedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('purpose', models.CharField(choices=[('product_demo', 'Product Demo'), ('follow_up_call', 'Follow-Up Call'), ('contract_discussion', 'Contract Discussion'), ('sales_meeting', 'Sales Meeting'), ('support_call', 'Support Call'), ('other', 'Other')], max_length=200)),
('scheduled_at', models.DateTimeField()),
('scheduled_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email')], default='Call', max_length=200)),
('completed', models.BooleanField(default=False, verbose_name='Completed')),
('duration', models.DurationField(default=datetime.timedelta(seconds=300))),
('notes', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='Scheduled', max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='django_ledger.customermodel')),
('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='inventory.lead')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer')),
('scheduled_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
@ -836,6 +841,24 @@ class Migration(migrations.Migration):
'unique_together': {('dealer', 'car_make')},
},
),
migrations.CreateModel(
name='ExtraInfo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.CharField(blank=True, max_length=255, null=True)),
('related_object_id', models.CharField(blank=True, max_length=255, null=True)),
('data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_info_primary', to='contenttypes.contenttype')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_extra_info', to=settings.AUTH_USER_MODEL)),
('related_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='extra_info_secondary', to='contenttypes.contenttype')),
],
options={
'verbose_name_plural': 'Extra Info',
'indexes': [models.Index(fields=['content_type', 'object_id'], name='inventory_e_content_2ecbed_idx'), models.Index(fields=['related_content_type', 'related_object_id'], name='inventory_e_related_8680bb_idx')],
},
),
migrations.CreateModel(
name='CarColors',
fields=[

View File

@ -1188,10 +1188,7 @@ class Staff(models.Model, LocalizedNameMixin):
@property
def groups(self):
return [x.customgroup for x in self.user.groups.all()]
@property
def groups(self):
return [x.customgroup for x in self.user.groups.all()]
return CustomGroup.objects.filter(pk__in=[x.customgroup.pk for x in self.user.groups.all()])
def clear_groups(self):
self.remove_superuser_permission()
@ -2568,16 +2565,8 @@ class CustomGroup(models.Model):
pass
def set_default_permissions(self):
Permission.objects.get_or_create(name="Can approve estimate",codename="can_approve_estimatemodel",content_type=ContentType.objects.get_for_model(EstimateModel))
Permission.objects.get_or_create(name="Can approve bill",codename="can_approve_billmodel",content_type=ContentType.objects.get_for_model(BillModel))
Permission.objects.get_or_create(name="Can view inventory",codename="can_view_inventory",content_type=ContentType.objects.get_for_model(Car))
Permission.objects.get_or_create(name="Can view sales",codename="can_view_sales",content_type=ContentType.objects.get_for_model(EstimateModel))
Permission.objects.get_or_create(name="Can view crm",codename="can_view_crm",content_type=ContentType.objects.get_for_model(Lead))
Permission.objects.get_or_create(name="Can view financials",codename="can_view_financials",content_type=ContentType.objects.get_for_model(AccountModel))
Permission.objects.get_or_create(name="Can view reports",codename="can_view_reports",content_type=ContentType.objects.get_for_model(LedgerModel))
self.clear_permissions()
######################################
######################################
#MANAGER

View File

@ -171,7 +171,7 @@ def create_ledger_entity(sender, instance, created, **kwargs):
entity.create_uom(name=u[1], unit_abbr=u[0])
# Create COA accounts, background task
async_task(create_coa_accounts,instance.pk)
async_task(create_coa_accounts,instance)
# create_settings(instance.pk)
# create_accounts_for_make(instance.pk)
@ -192,20 +192,20 @@ def create_dealer_groups(sender, instance, created, **kwargs):
:param kwargs: Additional keyword arguments passed by the signal.
:type kwargs: dict
"""
group_names = ["Inventory", "Accountant", "Sales","Manager"]
if created:
# async_task("inventory.tasks.create_groups",instance.slug)
def create_groups():
for group_name in ["Inventory", "Accountant", "Sales","Manager"]:
group= Group.objects.create(
name=f"{instance.slug}_{group_name}"
)
group_manager = models.CustomGroup.objects.create(
name=group_name, dealer=instance, group=group
)
group_manager.set_default_permissions()
instance.user.groups.add(group)
def create_groups():
for group_name in group_names:
group, created = Group.objects.get_or_create(
name=f"{instance.slug}_{group_name}"
)
group_manager, created = models.CustomGroup.objects.get_or_create(
name=group_name, dealer=instance, group=group
)
group_manager.set_default_permissions()
instance.user.groups.add(group)
transaction.on_commit(create_groups)
transaction.on_commit(create_groups)
# Create Vendor
@ -739,24 +739,24 @@ def create_dealer_settings(sender, instance, created, **kwargs):
# VatRate.objects.get_or_create(rate=Decimal('0.15'), is_active=True)
@receiver(post_save, sender=models.Dealer)
def create_make_ledger_accounts(sender, instance, created, **kwargs):
"""
Signal receiver that creates ledger accounts for car makes associated with a dealer when a new dealer instance
is created. This function listens to the `post_save` signal of the `Dealer` model and automatically generates
new ledger accounts for all car makes, associating them with the given dealer's entity.
# @receiver(post_save, sender=models.Dealer)
# def create_make_ledger_accounts(sender, instance, created, **kwargs):
# """
# Signal receiver that creates ledger accounts for car makes associated with a dealer when a new dealer instance
# is created. This function listens to the `post_save` signal of the `Dealer` model and automatically generates
# new ledger accounts for all car makes, associating them with the given dealer's entity.
:param sender: The model class (`Dealer`) that triggered the signal.
:type sender: Type[models.Dealer]
:param instance: The instance of the `Dealer` model that triggered the signal.
:param created: A boolean indicating whether a new `Dealer` instance was created.
:type created: bool
:param kwargs: Additional keyword arguments passed by the signal.
:return: None
"""
if created:
entity = instance.entity
coa = entity.get_default_coa()
# :param sender: The model class (`Dealer`) that triggered the signal.
# :type sender: Type[models.Dealer]
# :param instance: The instance of the `Dealer` model that triggered the signal.
# :param created: A boolean indicating whether a new `Dealer` instance was created.
# :type created: bool
# :param kwargs: Additional keyword arguments passed by the signal.
# :return: None
# """
# if created:
# entity = instance.entity
# coa = entity.get_default_coa()
# for make in models.CarMake.objects.all():
# last_account = entity.get_all_accounts().filter(role=roles.ASSET_CA_RECEIVABLES).order_by('-created').first()

View File

@ -33,9 +33,8 @@ def create_settings(pk):
)
def create_coa_accounts(pk):
def create_coa_accounts(instance):
with transaction.atomic():
instance = Dealer.objects.select_for_update().get(pk=pk)
entity = instance.entity
coa = entity.get_default_coa()
@ -1483,3 +1482,21 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
address=address,
)
# def create_groups(dealer_slug):
# from inventory.models import CustomGroup
# instance = Dealer.objects.get(slug=dealer_slug)
# def run():
# for group_name in ["Inventory", "Accountant", "Sales", "Manager"]:
# group, created = Group.objects.get_or_create(
# name=f"{instance.slug}_{group_name}"
# )
# group_manager, created = CustomGroup.objects.get_or_create(
# name=group_name, dealer=instance, group=group
# )
# if created:
# group_manager.set_default_permissions()
# instance.user.groups.add(group)
# transaction.on_commit(run)

View File

@ -473,6 +473,15 @@ def po_item_formset_table(context, po_model, itemtxs_formset,user):
@register.inclusion_tag("bill/tags/bill_item_formset.html", takes_context=True)
def bill_item_formset_table(context, item_formset):
for item in item_formset:
if item:
item.initial['quantity'] = item.instance.po_quantity
item.initial['unit_cost'] = item.instance.po_unit_cost
# print(item.instance.po_quantity)
# print(item.instance.po_unit_cost)
# print(item.instance.po_total_amount)
# if item.po_quantity:
# item.quantity = item.po_quantity
return {
"dealer_slug": context["view"].kwargs["dealer_slug"],
"entity_slug": context["view"].kwargs["entity_slug"],

View File

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

View File

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

View File

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

View File

@ -109,3 +109,25 @@ html[dir="rtl"] .form-icon-container .form-control {
padding-left: 10px;
}
.submitBtn.loading {
position: relative;
opacity: 0.8;
}
.submitBtn.loading:after {
content: "";
position: absolute;
right: 10px;
top: 50%;
width: 16px;
height: 16px;
margin-top: -8px;
border: 2px solid #fff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

View File

@ -1,3 +1,29 @@
document.addEventListener('DOMContentLoaded', function() {
// Find all forms with class 'disable-on-submit' or target a specific form
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
// Find the submit button within this form
const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
if (submitButton) {
// Disable the button
submitButton.disabled = true;
// Optional: Add a loading class for styling
submitButton.classList.add('loading');
// Re-enable the button if the form submission fails
// This ensures the button doesn't stay disabled if there's an error
window.addEventListener('unload', function() {
submitButton.disabled = false;
submitButton.classList.remove('loading');
});
}
});
});
});
function getCookie(name) {
let cookieValue = null;
@ -222,3 +248,4 @@ const getDataTableInit = () => {
}
};

View File

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

View File

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

View File

@ -449,17 +449,6 @@ body.popup .submit-row {
_width: 700px;
}
.inline-group ul.tools {
padding: 0;
margin: 0;
list-style: none;
}
.inline-group ul.tools li {
display: inline;
padding: 0 5px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
color: var(--body-quiet-color);
@ -473,11 +462,8 @@ body.popup .submit-row {
border-bottom: 1px solid var(--hairline-color);
}
.inline-group ul.tools a.add,
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
padding-left: 16px;
font-size: 0.75rem;
}

View File

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

View File

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

View File

@ -220,34 +220,36 @@ fieldset .fieldBox {
}
.selector-add {
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
background-size: 24px auto;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -80px;
:enabled.selector-add:focus, :enabled.selector-add:hover {
background-position: 0 -120px;
}
.selector-remove {
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
background: url(../img/selector-icons.svg) 0 -144px no-repeat;
background-size: 24px auto;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -112px;
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
background-position: 0 -168px;
}
a.selector-chooseall {
.selector-chooseall {
background: url(../img/selector-icons.svg) right -128px no-repeat;
}
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover {
background-position: 100% -144px;
}
a.selector-clearall {
.selector-clearall {
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
}
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
:enabled.selector-clearall:focus, :enabled.selector-clearall:hover {
background-position: 0 -176px;
}

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 560 B

After

Width:  |  Height:  |  Size: 537 B

View File

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

View File

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

View File

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

View File

@ -109,3 +109,25 @@ html[dir="rtl"] .form-icon-container .form-control {
padding-left: 10px;
}
.submitBtn.loading {
position: relative;
opacity: 0.8;
}
.submitBtn.loading:after {
content: "";
position: absolute;
right: 10px;
top: 50%;
width: 16px;
height: 16px;
margin-top: -8px;
border: 2px solid #fff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,3 +1,29 @@
document.addEventListener('DOMContentLoaded', function() {
// Find all forms with class 'disable-on-submit' or target a specific form
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
// Find the submit button within this form
const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
if (submitButton) {
// Disable the button
submitButton.disabled = true;
// Optional: Add a loading class for styling
submitButton.classList.add('loading');
// Re-enable the button if the form submission fails
// This ensures the button doesn't stay disabled if there's an error
window.addEventListener('unload', function() {
submitButton.disabled = false;
submitButton.classList.remove('loading');
});
}
});
});
});
function getCookie(name) {
let cookieValue = null;
@ -222,3 +248,4 @@ const getDataTableInit = () => {
}
};

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
{% block content %}
<div class="container py-4">
<div class="row g-2">
<!-- Bill Form -->
<div class="col-12">
@ -18,25 +18,38 @@
<div class="card mb-2">
<div class="card-body">
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %}
<a href="{% url 'bill-detail' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}"
class="btn btn-phoenix-secondary w-100 mb-2">
<i class="fas fa-arrow-left me-2"></i>{% trans 'Back to Bill Detail' %}
</a>
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
{% csrf_token %}
<div class="mb-3">
{{ form|crispy }}
</div>
<button type="submit" class="btn btn-phoenix-primary w-100 mb-2">
<i class="fas fa-save me-2"></i>{% trans 'Save Bill' %}
</button>
<a href="{% url
<a href="{% url 'bill-detail' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}"
class="btn btn-phoenix-secondary w-100 mb-2">
<i class="fas fa-arrow-left me-2"></i>{% trans 'Back to Bill Detail' %}
</a>
<a href="{% url 'bill_list' request.dealer.slug %}"
class="btn btn-phoenix-info w-100 mb-2">
<i class="fas fa-list me-2"></i>{% trans 'Bill List' %}
</a>
</form>
<a href="{% url 'bill_list' request.dealer.slug %}"
class="btn btn-phoenix-info w-100 mb-2">
<i class="fas fa-list me-2"></i>{% trans 'Bill List' %}
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Bill Item Formset -->
<div class="col-12">
{% bill_item_formset_table itemtxs_formset %}

View File

@ -101,7 +101,7 @@
<!-- Total Amount -->
<td class="text-end">
<span class="text-xs font-weight-bold">
{% currency_symbol %}{{ f.instance.total_amount | currency_format }}
<span>{% currency_symbol %}</span>{{ f.instance.total_amount | currency_format }}
</span>
</td>

View File

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

View File

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

View File

@ -82,7 +82,7 @@
</div>
</div>
<div class="d-flex align-items-center gap-2">
{% if perms.django_ledger.change_invoicemodel%}
{% if perms.django_ledger.add_payment%}
{% if invoice.invoice_status == 'in_review' %}
<button id="accept_invoice" class="btn btn-phoenix-secondary" data-bs-toggle="modal" data-bs-target="#confirmModal"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-check-double"></i> {% trans 'Accept' %}</span></button>
{% endif %}
@ -98,7 +98,7 @@
{% endif %}
{% endif %}
<a href="{% url 'invoice_preview' request.dealer.slug invoice.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block"><i class="fa-regular fa-eye"></i> {% trans 'Preview' %}</span></a>
</div>
</div>
{{invoice.amount_owned}}

View File

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