diff --git a/db.sqlite3-shm b/db.sqlite3-shm new file mode 100644 index 00000000..87689361 Binary files /dev/null and b/db.sqlite3-shm differ diff --git a/db.sqlite3-wal b/db.sqlite3-wal new file mode 100644 index 00000000..e69de29b diff --git a/haikalbot/migrations/0002_initial.py b/haikalbot/migrations/0002_initial.py index 7ac265cf..46ca49a3 100644 --- a/haikalbot/migrations/0002_initial.py +++ b/haikalbot/migrations/0002_initial.py @@ -1,7 +1,7 @@ # Generated by Django 5.1.4 on 2025-01-12 17:20 -import django.db.models.deletion from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -9,8 +9,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('haikalbot', '0001_initial'), ('inventory', '0001_initial'), + ('haikalbot', '0001_initial'), ] operations = [ diff --git a/inventory/forms.py b/inventory/forms.py index 41e5bb3a..1d9083b4 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -8,6 +8,7 @@ from phonenumber_field.phonenumber import PhoneNumber from .mixins import AddClassMixin from django.forms.models import inlineformset_factory +from django_ledger.forms.invoice import InvoiceModelCreateForm as InvoiceModelCreateFormBase from .models import ( Dealer, # Branch, @@ -556,10 +557,22 @@ class PaymentForm(forms.Form): label="Payment Method", required=True, ) - payment_date = forms.DateField(label="Payment Date", required=True) + payment_date = forms.DateField(label="Payment Date",widget=DateInput(attrs={'type': 'date'}), required=True) + + def clean_amount(self): + invoice = self.cleaned_data['invoice'] + amount = self.cleaned_data['amount'] + if amount < invoice.amount_due: + raise forms.ValidationError("Payment amount is greater than invoice amount due") + if amount <= 0: + raise forms.ValidationError("Payment amount must be greater than 0") + if invoice.amount_due == invoice.amount_paid or invoice.invoice_status == "paid": + raise forms.ValidationError("Invoice is already paid") + if amount > invoice.amount_due: + raise forms.ValidationError("Payment amount is greater than invoice amount due") + return amount - from django import forms class EmailForm(forms.Form): subject = forms.CharField(max_length=255) @@ -611,4 +624,14 @@ class ActivityForm(forms.ModelForm): class OpportunityForm(forms.ModelForm): class Meta: model = Opportunity - fields = ['customer', 'car', 'stage', 'probability', 'closing_date'] \ No newline at end of file + fields = ['customer', 'car', 'stage', 'probability', 'closing_date'] + + +class InvoiceModelCreateForm(InvoiceModelCreateFormBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields['cash_account'].widget = forms.HiddenInput() + self.fields['prepaid_account'].widget = forms.HiddenInput() + self.fields['unearned_account'].widget = forms.HiddenInput() + self.fields['date_draft'] = forms.DateField(widget=DateInput(attrs={'type': 'date'})) \ No newline at end of file diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py index a8b172e7..94b54d6e 100644 --- a/inventory/migrations/0001_initial.py +++ b/inventory/migrations/0001_initial.py @@ -1,12 +1,12 @@ # Generated by Django 5.1.4 on 2025-01-12 17:20 +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models import django.db.models.deletion import inventory.mixins import inventory.models import phonenumber_field.modelfields -from decimal import Decimal -from django.conf import settings -from django.db import migrations, models class Migration(migrations.Migration): @@ -16,10 +16,26 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), ('django_ledger', '0017_alter_accountmodel_unique_together_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ + migrations.CreateModel( + name='AdditionalServices', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')), + ('description', models.TextField(verbose_name='Description')), + ('price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Price')), + ('taxable', models.BooleanField(default=False, verbose_name='taxable')), + ('uom', models.CharField(choices=[('EA', 'Each'), ('PR', 'Pair'), ('SET', 'Set'), ('GAL', 'Gallon'), ('L', 'Liter'), ('M', 'Meter'), ('KG', 'Kilogram'), ('HR', 'Hour'), ('BX', 'Box'), ('RL', 'Roll'), ('PKG', 'Package'), ('DZ', 'Dozen'), ('SQ_M', 'Square Meter'), ('PC', 'Piece'), ('BDL', 'Bundle')], max_length=10, verbose_name='Unit of Measurement')), + ], + options={ + 'verbose_name': 'Additional Services', + 'verbose_name_plural': 'Additional Services', + }, + bases=(models.Model, inventory.mixins.LocalizedNameMixin), + ), migrations.CreateModel( name='Car', fields=[ @@ -203,11 +219,6 @@ class Migration(migrations.Migration): }, bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), - migrations.AddField( - model_name='car', - name='id_car_model', - field=models.ForeignKey(blank=True, db_column='id_car_model', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model'), - ), migrations.CreateModel( name='CarOption', fields=[ @@ -221,36 +232,6 @@ class Migration(migrations.Migration): }, bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), - migrations.CreateModel( - name='CarOptionValue', - fields=[ - ('id_car_option_value', models.AutoField(primary_key=True, serialize=False)), - ('value', models.CharField(max_length=500)), - ('unit', models.CharField(blank=True, max_length=255, null=True)), - ('is_base', models.IntegerField()), - ('id_car_equipment', models.ForeignKey(db_column='id_car_equipment', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carequipment')), - ('id_car_option', models.ForeignKey(db_column='id_car_option', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')), - ], - options={ - 'verbose_name': 'Option Value', - }, - ), - migrations.CreateModel( - name='CarRegistration', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('plate_number', models.IntegerField(verbose_name='Plate Number')), - ('text1', models.CharField(max_length=1, verbose_name='Text 1')), - ('text2', models.CharField(max_length=1, verbose_name='Text 2')), - ('text3', models.CharField(max_length=1, verbose_name='Text 3')), - ('registration_date', models.DateTimeField(verbose_name='Registration Date')), - ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='inventory.car', verbose_name='Car')), - ], - options={ - 'verbose_name': 'Registration', - 'verbose_name_plural': 'Registrations', - }, - ), migrations.CreateModel( name='CarSerie', fields=[ @@ -267,11 +248,6 @@ class Migration(migrations.Migration): }, bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), - migrations.AddField( - model_name='car', - name='id_car_serie', - field=models.ForeignKey(blank=True, db_column='id_car_serie', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie', verbose_name='Series'), - ), migrations.CreateModel( name='CarSpecification', fields=[ @@ -286,54 +262,26 @@ class Migration(migrations.Migration): bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), migrations.CreateModel( - name='CarTrim', - fields=[ - ('id_car_trim', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(blank=True, max_length=255, null=True)), - ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), - ('start_production_year', models.IntegerField(blank=True, null=True)), - ('end_production_year', models.IntegerField(blank=True, null=True)), - ('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')), - ], - options={ - 'verbose_name': 'Trim', - }, - bases=(models.Model, inventory.mixins.LocalizedNameMixin), - ), - migrations.CreateModel( - name='CarSpecificationValue', - fields=[ - ('id_car_specification_value', models.AutoField(primary_key=True, serialize=False)), - ('value', models.CharField(max_length=500)), - ('unit', models.CharField(blank=True, max_length=255, null=True)), - ('id_car_specification', models.ForeignKey(db_column='id_car_specification', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')), - ('id_car_trim', models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim')), - ], - options={ - 'verbose_name': 'Specification Value', - }, - ), - migrations.AddField( - model_name='carequipment', - name='id_car_trim', - field=models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim'), - ), - migrations.AddField( - model_name='car', - name='id_car_trim', - field=models.ForeignKey(blank=True, db_column='id_car_trim', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim', verbose_name='Trim'), - ), - migrations.CreateModel( - name='CustomCard', + name='Customer', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('custom_number', models.CharField(max_length=255, verbose_name='Custom Number')), - ('custom_date', models.DateField(verbose_name='Custom Date')), - ('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car')), + ('title', models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company'), ('na', 'N/A')], default='na', max_length=10, verbose_name='Title')), + ('first_name', models.CharField(max_length=50, verbose_name='First Name')), + ('middle_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='Middle Name')), + ('last_name', models.CharField(max_length=50, verbose_name='Last Name')), + ('gender', models.CharField(choices=[('m', 'Male'), ('f', 'Female')], max_length=1, verbose_name='Gender')), + ('dob', models.DateField(verbose_name='Date of Birth')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')), + ('national_id', models.CharField(max_length=10, unique=True, verbose_name='National ID')), + ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', unique=True, verbose_name='Phone Number')), + ('city', models.CharField(blank=True, max_length=255, verbose_name='City')), + ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), ], options={ - 'verbose_name': 'Custom Card', - 'verbose_name_plural': 'Custom Cards', + 'verbose_name': 'Customer', + 'verbose_name_plural': 'Customers', }, ), migrations.CreateModel( @@ -493,38 +441,19 @@ class Migration(migrations.Migration): bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), migrations.CreateModel( - name='Refund', + 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')), - ('reason', models.TextField(blank=True, verbose_name='reason')), - ('refund_date', models.DateField(auto_now_add=True, verbose_name='refund date')), - ('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='refund', to='inventory.payment')), + ('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': 'refund', - 'verbose_name_plural': 'refunds', + 'verbose_name': 'payment', + 'verbose_name_plural': 'payments', }, ), - migrations.CreateModel( - name='Representative', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')), - ('id_number', models.CharField(max_length=10, verbose_name='ID Number')), - ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), - ('email', models.EmailField(max_length=255, verbose_name='Email Address')), - ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), - ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='representatives', to='inventory.dealer')), - ('organization', models.ManyToManyField(related_name='representatives', to='inventory.organization')), - ], - options={ - 'verbose_name': 'Representative', - 'verbose_name_plural': 'Representatives', - }, - bases=(models.Model, inventory.mixins.LocalizedNameMixin), - ), migrations.CreateModel( name='SaleQuotation', fields=[ @@ -651,7 +580,6 @@ class Migration(migrations.Migration): ('billing_cycle', models.CharField(choices=[('monthly', 'Monthly'), ('annual', 'Annual')], default='monthly', help_text='Billing cycle for the subscription', max_length=10)), ('last_payment_date', models.DateField(blank=True, help_text='Date of the last payment made', null=True)), ('next_payment_date', models.DateField(blank=True, help_text='Date of the next payment due', null=True)), - ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='inventory.subscriptionplan')), ], options={ 'verbose_name': 'Subscription', @@ -659,35 +587,32 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='SubscriptionUser', + name='SubscriptionPlan', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.subscription')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('name', models.CharField(help_text='Name of the subscription plan', max_length=100, unique=True)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('max_users', models.PositiveIntegerField(default=1, help_text='Maximum number of users allowed')), + ('max_inventory_size', models.PositiveIntegerField(default=50, help_text='Maximum number of cars in inventory')), + ('support_level', models.CharField(choices=[('basic', 'Basic Support'), ('priority', 'Priority Support'), ('dedicated', 'Dedicated Support')], default='basic', help_text='Level of support provided', max_length=50)), + ('custom_features', models.JSONField(blank=True, help_text='Additional features specific to this plan', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), ], options={ - 'verbose_name': 'Subscription User', - 'verbose_name_plural': 'Subscription Users', + 'verbose_name': 'Subscription Plan', + 'verbose_name_plural': 'Subscription Plans', }, ), - migrations.AddField( - model_name='subscription', - name='users', - field=models.ManyToManyField(through='inventory.SubscriptionUser', to=settings.AUTH_USER_MODEL), - ), migrations.CreateModel( - name='UserActivityLog', + name='VatRate', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('action', models.TextField()), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('rate', models.DecimalField(decimal_places=2, default=Decimal('0.15'), max_digits=5)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), ], - options={ - 'verbose_name': 'User Activity Log', - 'verbose_name_plural': 'User Activity Logs', - 'ordering': ['-timestamp'], - }, ), migrations.CreateModel( name='Vendor', @@ -711,11 +636,387 @@ class Migration(migrations.Migration): }, bases=(models.Model, inventory.mixins.LocalizedNameMixin), ), + migrations.CreateModel( + name='UserActivityLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.TextField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'User Activity Log', + 'verbose_name_plural': 'User Activity Logs', + 'ordering': ['-timestamp'], + }, + ), + migrations.CreateModel( + name='SubscriptionUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subscription', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.subscription')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Subscription User', + 'verbose_name_plural': 'Subscription Users', + }, + ), + migrations.AddField( + model_name='subscription', + name='plan', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='inventory.subscriptionplan'), + ), + migrations.AddField( + model_name='subscription', + name='users', + field=models.ManyToManyField(through='inventory.SubscriptionUser', to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name='Staff', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')), + ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), + ('staff_type', models.CharField(choices=[('manager', 'Manager'), ('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales'), ('coordinator', 'Coordinator'), ('receptionist', 'Receptionist'), ('agent', 'Agent')], max_length=255, verbose_name='Staff Type')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to='inventory.dealer')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Staff', + 'verbose_name_plural': 'Staff', + 'permissions': [], + }, + bases=(models.Model, inventory.mixins.LocalizedNameMixin), + managers=[ + ('objects', inventory.models.StaffUserManager()), + ], + ), + migrations.CreateModel( + name='SalesOrder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Total Amount')), + ('quotation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sales_order', to='inventory.salequotation', verbose_name='Quotation')), + ], + ), + migrations.CreateModel( + name='SaleQuotationCar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1, verbose_name='Quantity')), + ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.car', verbose_name='Car')), + ('quotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quotation_cars', to='inventory.salequotation', verbose_name='Quotation')), + ], + ), + migrations.CreateModel( + name='Representative', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('arabic_name', models.CharField(max_length=255, verbose_name='Arabic Name')), + ('id_number', models.CharField(max_length=10, verbose_name='ID Number')), + ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='SA', verbose_name='Phone Number')), + ('email', models.EmailField(max_length=255, verbose_name='Email Address')), + ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='Address')), + ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='representatives', to='inventory.dealer')), + ('organization', models.ManyToManyField(related_name='representatives', to='inventory.organization')), + ], + options={ + 'verbose_name': 'Representative', + 'verbose_name_plural': 'Representatives', + }, + bases=(models.Model, inventory.mixins.LocalizedNameMixin), + ), + migrations.CreateModel( + name='Refund', + 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')), + ('reason', models.TextField(blank=True, verbose_name='reason')), + ('refund_date', models.DateField(auto_now_add=True, verbose_name='refund date')), + ('payment', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='refund', to='inventory.payment')), + ], + options={ + 'verbose_name': 'refund', + 'verbose_name_plural': 'refunds', + }, + ), + migrations.AddField( + model_name='payment', + name='quotation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='inventory.salequotation'), + ), + migrations.CreateModel( + name='Opportunity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stage', models.CharField(choices=[('prospect', 'Prospect'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], max_length=20, verbose_name='Stage')), + ('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], default='new', max_length=20, verbose_name='Status')), + ('probability', models.PositiveIntegerField(validators=[inventory.models.validate_probability])), + ('closing_date', models.DateField(verbose_name='Closing Date')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('closed', models.BooleanField(default=False, verbose_name='Closed')), + ('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.customer')), + ('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer')), + ('staff', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner')), + ], + options={ + 'verbose_name': 'Opportunity', + 'verbose_name_plural': 'Opportunities', + }, + ), + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message', models.CharField(max_length=255, verbose_name='Message')), + ('is_read', models.BooleanField(default=False, verbose_name='Is Read')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Notification', + 'verbose_name_plural': 'Notifications', + 'ordering': ['-created'], + }, + ), + migrations.CreateModel( + name='Notes', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('note', models.TextField(verbose_name='Note')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Note', + 'verbose_name_plural': 'Notes', + }, + ), + migrations.CreateModel( + name='LeadStatusHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('old_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status')), + ('new_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status')), + ('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='Changed At')), + ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='status_changes', to='inventory.staff')), + ('lead', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='status_history', to='inventory.lead')), + ], + options={ + 'verbose_name': 'Lead Status History', + 'verbose_name_plural': 'Lead Status Histories', + }, + ), + migrations.AddField( + model_name='lead', + name='assigned', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned'), + ), + migrations.AddField( + model_name='lead', + name='dealer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer'), + ), + migrations.AddField( + model_name='lead', + name='id_car_make', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make'), + ), + migrations.AddField( + model_name='lead', + name='id_car_model', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model'), + ), + migrations.AddField( + model_name='customer', + name='dealer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='inventory.dealer'), + ), + migrations.AddField( + model_name='customer', + name='lead', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted', to='inventory.lead', verbose_name='Lead'), + ), + migrations.AddField( + model_name='customer', + name='staff', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer_staff', to='inventory.staff', verbose_name='Staff'), + ), + migrations.CreateModel( + name='CustomCard', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('custom_number', models.CharField(max_length=255, verbose_name='Custom Number')), + ('custom_date', models.DateField(verbose_name='Custom Date')), + ('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_cards', to='inventory.car', verbose_name='Car')), + ], + options={ + 'verbose_name': 'Custom Card', + 'verbose_name_plural': 'Custom Cards', + }, + ), + migrations.CreateModel( + name='CarTrim', + fields=[ + ('id_car_trim', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=255, null=True)), + ('arabic_name', models.CharField(blank=True, max_length=255, null=True)), + ('start_production_year', models.IntegerField(blank=True, null=True)), + ('end_production_year', models.IntegerField(blank=True, null=True)), + ('id_car_serie', models.ForeignKey(db_column='id_car_serie', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie')), + ], + options={ + 'verbose_name': 'Trim', + }, + bases=(models.Model, inventory.mixins.LocalizedNameMixin), + ), + migrations.CreateModel( + name='CarSpecificationValue', + fields=[ + ('id_car_specification_value', models.AutoField(primary_key=True, serialize=False)), + ('value', models.CharField(max_length=500)), + ('unit', models.CharField(blank=True, max_length=255, null=True)), + ('id_car_specification', models.ForeignKey(db_column='id_car_specification', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carspecification')), + ('id_car_trim', models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim')), + ], + options={ + 'verbose_name': 'Specification Value', + }, + ), + migrations.CreateModel( + name='CarRegistration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('plate_number', models.IntegerField(verbose_name='Plate Number')), + ('text1', models.CharField(max_length=1, verbose_name='Text 1')), + ('text2', models.CharField(max_length=1, verbose_name='Text 2')), + ('text3', models.CharField(max_length=1, verbose_name='Text 3')), + ('registration_date', models.DateTimeField(verbose_name='Registration Date')), + ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='inventory.car', verbose_name='Car')), + ], + options={ + 'verbose_name': 'Registration', + 'verbose_name_plural': 'Registrations', + }, + ), + migrations.CreateModel( + name='CarOptionValue', + fields=[ + ('id_car_option_value', models.AutoField(primary_key=True, serialize=False)), + ('value', models.CharField(max_length=500)), + ('unit', models.CharField(blank=True, max_length=255, null=True)), + ('is_base', models.IntegerField()), + ('id_car_equipment', models.ForeignKey(db_column='id_car_equipment', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carequipment')), + ('id_car_option', models.ForeignKey(db_column='id_car_option', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.caroption')), + ], + options={ + 'verbose_name': 'Option Value', + }, + ), + migrations.CreateModel( + name='CarLocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, help_text='Optional description about the showroom placement.', null=True, verbose_name='Description')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Updated')), + ('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='location', to='inventory.car', verbose_name='Car')), + ('owner', models.ForeignKey(help_text='Dealer who owns the car.', on_delete=django.db.models.deletion.CASCADE, related_name='owned_cars', to='inventory.dealer', verbose_name='Owner')), + ('showroom', models.ForeignKey(help_text='Dealer where the car is displayed (can be the owner).', on_delete=django.db.models.deletion.CASCADE, related_name='showroom_cars', to='inventory.dealer', verbose_name='Showroom')), + ], + options={ + 'verbose_name': 'Car Location', + 'verbose_name_plural': 'Car Locations', + }, + ), + migrations.CreateModel( + name='CarFinance', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cost_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Cost Price')), + ('selling_price', models.DecimalField(decimal_places=2, max_digits=14, verbose_name='Selling Price')), + ('discount_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=14, verbose_name='Discount Amount')), + ('additional_services', models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices')), + ('car', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='finances', to='inventory.car')), + ], + options={ + 'verbose_name': 'Car Financial Details', + 'verbose_name_plural': 'Car Financial Details', + }, + ), + migrations.AddField( + model_name='carequipment', + name='id_car_trim', + field=models.ForeignKey(db_column='id_car_trim', on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim'), + ), + migrations.AddField( + model_name='car', + name='dealer', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.dealer', verbose_name='Dealer'), + ), + migrations.AddField( + model_name='car', + name='id_car_make', + field=models.ForeignKey(blank=True, db_column='id_car_make', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make'), + ), + migrations.AddField( + model_name='car', + name='id_car_model', + field=models.ForeignKey(blank=True, db_column='id_car_model', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model'), + ), + migrations.AddField( + model_name='car', + name='id_car_serie', + field=models.ForeignKey(blank=True, db_column='id_car_serie', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carserie', verbose_name='Series'), + ), + migrations.AddField( + model_name='car', + name='id_car_trim', + field=models.ForeignKey(blank=True, db_column='id_car_trim', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.cartrim', verbose_name='Trim'), + ), migrations.AddField( model_name='car', name='vendor', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='cars', to='inventory.vendor', verbose_name='Vendor'), ), + migrations.AddField( + model_name='additionalservices', + name='dealer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.dealer', verbose_name='Dealer'), + ), + migrations.AddField( + model_name='additionalservices', + name='item', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item'), + ), + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('activity_type', models.CharField(choices=[('call', 'Call'), ('sms', 'SMS'), ('email', 'Email'), ('whatsapp', 'WhatsApp'), ('visit', 'Visit'), ('add_car', 'Add Car'), ('reserve_car', 'Reserve Car'), ('remove_car', 'Remove Car'), ('create_quotation', 'Create Quotation'), ('cancel_quotation', 'Cancel Quotation'), ('create_order', 'Create Order'), ('cancel_order', 'Cancel Order'), ('create_invoice', 'Create Invoice'), ('cancel_invoice', 'Cancel Invoice')], max_length=50, verbose_name='Activity Type')), + ('notes', models.TextField(blank=True, null=True, verbose_name='Notes')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Activity', + 'verbose_name_plural': 'Activities', + }, + ), migrations.CreateModel( name='CarReservation', fields=[ diff --git a/inventory/migrations/0002_alter_carmake_car_type.py b/inventory/migrations/0002_alter_carmake_car_type.py new file mode 100644 index 00000000..06e7827a --- /dev/null +++ b/inventory/migrations/0002_alter_carmake_car_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2025-01-13 10:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='carmake', + name='car_type', + field=models.SmallIntegerField(blank=True, choices=[], null=True), + ), + ] diff --git a/inventory/migrations/0003_alter_carmake_car_type.py b/inventory/migrations/0003_alter_carmake_car_type.py new file mode 100644 index 00000000..c701974c --- /dev/null +++ b/inventory/migrations/0003_alter_carmake_car_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2025-01-14 12:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0002_alter_carmake_car_type'), + ] + + operations = [ + migrations.AlterField( + model_name='carmake', + name='car_type', + field=models.SmallIntegerField(blank=True, choices=[(1, 'Car'), (2, 'Light Commercial'), (3, 'Heavy-Duty Tractors'), (4, 'Trailers'), (5, 'Medium Trucks'), (6, 'Buses'), (20, 'Motorcycles'), (21, 'Buggy'), (22, 'Moto ATV'), (23, 'Scooters'), (24, 'Karting'), (25, 'ATV'), (26, 'Snowmobiles')], null=True), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index 61ca451b..e0278891 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -47,8 +47,7 @@ class StaffUserManager(UserManager): user = self.create_user(username=email, email=email, password=password, **extra_fields) Staff.objects.create(user=user, name=name, arabic_name=arabic_name, phone_number=phone_number, staff_type=staff_type, **extra_fields) return user - - + class UnitOfMeasure(models.TextChoices): EACH = 'EA', 'Each' PAIR = 'PR', 'Pair' @@ -98,7 +97,7 @@ class CarMake(models.Model, LocalizedNameMixin): arabic_name = models.CharField(max_length=255, blank=True, null=True) logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True) is_sa_import = models.BooleanField(default=False) - car_type = models.SmallIntegerField(choices=CarType.choices) + car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) def __str__(self): return self.name diff --git a/inventory/signals.py b/inventory/signals.py index f3d4425a..fca8c26d 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -132,6 +132,7 @@ def create_ledger_entity(sender, instance, created, **kwargs): asset_ca_inventory.role_default = True asset_ca_inventory.save() + # Prepaid Expenses Account asset_ca_prepaid = entity.create_account( coa_model=coa, @@ -143,31 +144,100 @@ def create_ledger_entity(sender, instance, created, **kwargs): ) asset_ca_prepaid.role_default = True asset_ca_prepaid.save() - - # Notes Receivable Account - asset_lti_notes_receivable = entity.create_account( + + # Employee Expenses Account + asset_ca_prepaid_employee = entity.create_account( + coa_model=coa, + code="1105", + role=roles.ASSET_CA_PREPAID, + name=_("Employee Advance"), + balance_type="debit", + active=True, + ) + + # Inventory Account + asset_ca_inventory = entity.create_account( + coa_model=coa, + code="1106", + role=roles.ASSET_CA_INVENTORY, + name=_("Inventory"), + balance_type="debit", + active=True, + ) + asset_ca_inventory.role_default = True + asset_ca_inventory.save() + + # VAT Payable Account + liability_ltl_vat_receivable = entity.create_account( + coa_model=coa, + code="1107", + role=roles.ASSET_CA_RECEIVABLES, + name=_("VAT Receivable"), + balance_type="debit", + active=True, + ) + + # Buildings Accumulated Depreciation Account + asset_ppe_buildings_accum_depreciation = entity.create_account( coa_model=coa, code="1201", - role=roles.ASSET_LTI_NOTES_RECEIVABLE, - name=_("Notes Receivable"), - balance_type="debit", + role=roles.ASSET_PPE_BUILDINGS_ACCUM_DEPRECIATION, + name=_("Buildings - Accum. Depreciation"), + balance_type="credit", active=True, ) - asset_lti_notes_receivable.role_default = True - asset_lti_notes_receivable.save() - - # Land Account - asset_lti_land = entity.create_account( + asset_ppe_buildings_accum_depreciation.role_default = True + asset_ppe_buildings_accum_depreciation.save() + + # intangible Account + asset_lti_land_intangable = entity.create_account( coa_model=coa, code="1202", - role=roles.ASSET_LTI_LAND, - name=_("Land"), + role=roles.ASSET_INTANGIBLE_ASSETS, + name=_("Intangible Assets"), balance_type="debit", active=True, ) - asset_lti_land.role_default = True - asset_lti_land.save() + asset_lti_land_intangable.role_default = True + asset_lti_land_intangable.save() + + # investment property Account + asset_lti_land_investment = entity.create_account( + coa_model=coa, + code="1204", + role=roles.ASSET_LTI_SECURITIES, + name=_("Investments"), + balance_type="debit", + active=True, + ) + asset_lti_land_investment.role_default = True + asset_lti_land_investment.save() + + # # Notes Receivable Account + # asset_lti_notes_receivable = entity.create_account( + # coa_model=coa, + # code="1201", + # role=roles.ASSET_LTI_NOTES_RECEIVABLE, + # name=_("Notes Receivable"), + # balance_type="debit", + # active=True, + # ) + # asset_lti_notes_receivable.role_default = True + # asset_lti_notes_receivable.save() + # # Land Account + # asset_lti_land = entity.create_account( + # coa_model=coa, + # code="1202", + # role=roles.ASSET_LTI_LAND, + # name=_("Land"), + # balance_type="debit", + # active=True, + # ) + # asset_lti_land.role_default = True + # asset_lti_land.save() + + # Buildings Account asset_ppe_buildings = entity.create_account( coa_model=coa, @@ -180,17 +250,7 @@ def create_ledger_entity(sender, instance, created, **kwargs): asset_ppe_buildings.role_default = True asset_ppe_buildings.save() - # Buildings Accumulated Depreciation Account - asset_ppe_buildings_accum_depreciation = entity.create_account( - coa_model=coa, - code="1302", - role=roles.ASSET_PPE_BUILDINGS_ACCUM_DEPRECIATION, - name=_("Buildings - Accum. Depreciation"), - balance_type="credit", - active=True, - ) - asset_ppe_buildings_accum_depreciation.role_default = True - asset_ppe_buildings_accum_depreciation.save() + # Accounts Payable Account liability_cl_acc_payable = entity.create_account( @@ -239,7 +299,40 @@ def create_ledger_entity(sender, instance, created, **kwargs): ) liability_ltl_notes_payable.role_default = True liability_ltl_notes_payable.save() + + # VAT Payable Account + liability_ltl_vat_payable = entity.create_account( + coa_model=coa, + code="2106", + role=roles.LIABILITY_CL_OTHER, + name=_("VAT Payable"), + balance_type="credit", + active=True, + ) + + # taxes Payable Account + liability_ltl_taxes_payable = entity.create_account( + coa_model=coa, + code="2107", + role=roles.LIABILITY_CL_OTHER, + name=_("Taxes Payable"), + balance_type="credit", + active=True, + ) + + # social insurance Payable Account + liability_ltl_social_insurance_payable = entity.create_account( + coa_model=coa, + code="2108", + role=roles.LIABILITY_LTL_NOTES_PAYABLE, + name=_("Social Insurance Payable"), + balance_type="credit", + active=True, + ) + # End of Service Benefits + entity.create_account(coa_model=coa, code="2202", role=roles.LIABILITY_LTL_NOTES_PAYABLE, name=_("End of Service Benefits"), balance_type="credit", active=True) + # Mortgage Payable Account liability_ltl_mortgage_payable = entity.create_account( coa_model=coa, @@ -252,29 +345,43 @@ def create_ledger_entity(sender, instance, created, **kwargs): liability_ltl_mortgage_payable.role_default = True liability_ltl_mortgage_payable.save() - # Common Stock Account - equity_common_stock = entity.create_account( - coa_model=coa, - code="3101", - role=roles.EQUITY_COMMON_STOCK, - name=_("Common Stock"), - balance_type="credit", - active=True, - ) - equity_common_stock.role_default = True - equity_common_stock.save() - + # Capital + equity_capital = entity.create_account(coa_model=coa, code="3101", role=roles.EQUITY_CAPITAL, name=_("Registered Capital"), balance_type="credit", active=True) + equity_capital.role_default = True + equity_capital.save() + entity.create_account(coa_model=coa, code="3102", role=roles.EQUITY_CAPITAL, name=_("Additional Paid-In Capital"), balance_type="credit", active=True) + + # Other Equity + other_equity = entity.create_account(coa_model=coa, code="3201", role=roles.EQUITY_COMMON_STOCK, name=_("Opening Balances"), balance_type="credit", active=True) + other_equity.role_default = True + other_equity.save() + + # Reserves + reserve = entity.create_account(coa_model=coa, code="3301", role=roles.EQUITY_ADJUSTMENT, name=_("Statutory Reserve"), balance_type="credit", active=True) + reserve.role_default = True + reserve.save() + entity.create_account(coa_model=coa, code="3302", role=roles.EQUITY_ADJUSTMENT, name=_("Foreign Currency Translation Reserve"), balance_type="credit", active=True) + # Retained Earnings Account equity_retained_earnings = entity.create_account( coa_model=coa, - code="3102", - role=roles.EQUITY_ADJUSTMENT, - name=_("Retained Earnings"), + code="3401", + role=roles.EQUITY_PREFERRED_STOCK, + name=_("Operating Profits and Losses"), balance_type="credit", active=True, ) equity_retained_earnings.role_default = True equity_retained_earnings.save() + + equity_retained_earnings_losses = entity.create_account( + coa_model=coa, + code="3402", + role=roles.EQUITY_PREFERRED_STOCK, + name=_("Retained Earnings (or Losses)"), + balance_type="credit", + active=True, + ) # Sales Revenue Account income_operational = entity.create_account( @@ -300,6 +407,23 @@ def create_ledger_entity(sender, instance, created, **kwargs): income_interest.role_default = True income_interest.save() + # Uneared Income Account + income_unearned = entity.create_account( + coa_model=coa, + code="4103", + role=roles.INCOME_OTHER, + name=_("Unearned Income"), + balance_type="credit", + active=True, + ) + + # Operating Revenues + entity.create_account(coa_model=coa, code="4104", role=roles.INCOME_OPERATIONAL, name=_("Sales/Service Revenue"), balance_type="credit", active=True) + + #Non-Operating Revenues + entity.create_account(coa_model=coa, code="4201", role=roles.INCOME_OTHER, name=_("Non-Operating Revenues"), balance_type="credit", active=True) + + # Cost of Goods Sold (COGS) Account expense_cogs = entity.create_account( coa_model=coa, @@ -311,7 +435,28 @@ def create_ledger_entity(sender, instance, created, **kwargs): ) expense_cogs.role_default = True expense_cogs.save() - + + + # accrued Expenses Account + expense_cogs = entity.create_account( + coa_model=coa, + code="6117", + role=roles.EXPENSE_OPERATIONAL, + name=_("Accrued Expenses"), + balance_type="debit", + active=True, + ) + + # accrued salaries Account + expense_cogs = entity.create_account( + coa_model=coa, + code="6118", + role=roles.EXPENSE_OPERATIONAL, + name=_("Accrued Salaries"), + balance_type="debit", + active=True, + ) + # Rent Expense Account expense_rent = entity.create_account( coa_model=coa, @@ -323,7 +468,161 @@ def create_ledger_entity(sender, instance, created, **kwargs): ) expense_rent.role_default = True expense_rent.save() - + + # Salaries and Administrative Fees + expense_salaries = entity.create_account( + coa_model=coa, + code="6103", + role=roles.EXPENSE_OPERATIONAL, + name=_("Salaries and Administrative Fees"), + balance_type="debit", + active=True, + ) + + # Medical Insurance + expense_medical_insurance = entity.create_account( + coa_model=coa, + code="6104", + role=roles.EXPENSE_OPERATIONAL, + name=_("Medical Insurance"), + balance_type="debit", + active=True, + ) + + # Marketing and Advertising Expenses + expense_marketing = entity.create_account( + coa_model=coa, + code="6105", + role=roles.EXPENSE_OPERATIONAL, + name=_("Marketing and Advertising Expenses"), + balance_type="debit", + active=True, + ) + + # Commissions and Incentives + expense_commissions = entity.create_account( + coa_model=coa, + code="6106", + role=roles.EXPENSE_OPERATIONAL, + name=_("Commissions and Incentives"), + balance_type="debit", + active=True, + ) + + # Travel Tickets + expense_travel = entity.create_account( + coa_model=coa, + code="6107", + role=roles.EXPENSE_OPERATIONAL, + name=_("Travel Tickets"), + balance_type="debit", + active=True, + ) + + # Social Insurance + expense_other = entity.create_account( + coa_model=coa, + code="6108", + role=roles.EXPENSE_OPERATIONAL, + name=_("Social Insurance"), + balance_type="debit", + active=True, + ) + + # Government Fees + expense_other = entity.create_account( + coa_model=coa, + code="6109", + role=roles.EXPENSE_OPERATIONAL, + name=_("Government Fees"), + balance_type="debit", + active=True, + ) + + # Fees and Subscriptions + expense_other = entity.create_account( + coa_model=coa, + code="6110", + role=roles.EXPENSE_OPERATIONAL, + name=_("Fees and Subscriptions"), + balance_type="debit", + active=True, + ) + + # Office Services Expenses + expense_other = entity.create_account( + coa_model=coa, + code="6111", + role=roles.EXPENSE_OPERATIONAL, + name=_("Office Services Expenses"), + balance_type="debit", + active=True, + ) + + # Office Supplies and Printing + expense_other = entity.create_account( + coa_model=coa, + code="6112", + role=roles.EXPENSE_OPERATIONAL, + name=_("Office Supplies and Printing"), + balance_type="debit", + active=True, + ) + + # Hospitality Expenses + expense_other = entity.create_account( + coa_model=coa, + code="6113", + role=roles.EXPENSE_OPERATIONAL, + name=_("Hospitality Expenses"), + balance_type="debit", + active=True, + ) + + # Bank Commissions + expense_other = entity.create_account( + coa_model=coa, + code="6114", + role=roles.EXPENSE_OPERATIONAL, + name=_("Bank Commissions"), + balance_type="debit", + active=True, + ) + + # Other Expenses + expense_other = entity.create_account( + coa_model=coa, + code="6115", + role=roles.EXPENSE_OPERATIONAL, + name=_("Other Expenses"), + balance_type="debit", + active=True, + ) + + # Transportation Expenses + expense_other = entity.create_account( + coa_model=coa, + code="6116", + role=roles.EXPENSE_OPERATIONAL, + name=_("Transportation Expenses"), + balance_type="debit", + active=True, + ) + + # 5.1 Direct Costs + entity.create_account(coa_model=coa, code="6201", role=roles.EXPENSE_OPERATIONAL, name=_("Cost of Goods Sold"), balance_type="debit", active=True) + entity.create_account(coa_model=coa, code="6202", role=roles.EXPENSE_OPERATIONAL, name=_("Salaries and Wages"), balance_type="debit", active=True) + entity.create_account(coa_model=coa, code="6203", role=roles.EXPENSE_OPERATIONAL, name=_("Sales Commissions"), balance_type="debit", active=True) + entity.create_account(coa_model=coa, code="6204", role=roles.EXPENSE_OPERATIONAL, name=_("Shipping and Customs Clearance"), balance_type="debit", active=True) + + # 5.3 Non-Operating Expenses + entity.create_account(coa_model=coa, code="6301", role=roles.EXPENSE_OTHER, name=_("Zakat"), balance_type="debit", active=True) + entity.create_account(coa_model=coa, code="6302", role=roles.EXPENSE_OTHER, name=_("Taxes"), balance_type="debit", active=True) + entity.create_account(coa_model=coa, code="6303", role=roles.EXPENSE_OTHER, name=_("Foreign Currency Translation"), balance_type="debit", active=True) + entity.create_account(coa_model=coa, code="6304", role=roles.EXPENSE_OTHER, name=_("Interest Expenses"), balance_type="debit", active=True) + + + # Create Vendor @receiver(post_save, sender=models.Vendor) def create_ledger_vendor(sender, instance, created, **kwargs): diff --git a/inventory/urls.py b/inventory/urls.py index 218b75f7..0b12fcc3 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -164,6 +164,7 @@ urlpatterns = [ path('sales/payments//create/', views.PaymentCreateView, name='payment_create'), path('sales/payments/create/', views.PaymentCreateView, name='payment_create'), path('sales/payments//payment_details/', views.PaymentDetailView, name='payment_details'), + path('sales/payments//payment_mark_as_paid/', views.payment_mark_as_paid, name='payment_mark_as_paid'), # path('sales/payments//update/', views.JournalEntryUpdateView.as_view(), name='payment_update'), # path('sales/payments//delete/', views.JournalEntryDeleteView.as_view(), name='payment_delete'), # path('sales/payments//preview/', views.JournalEntryPreviewView.as_view(), name='payment_preview'), @@ -173,6 +174,7 @@ urlpatterns = [ # Items path('items/services/', views.ItemServiceListView.as_view(), name='item_service_list'), path('items/services/create/', views.ItemServiceCreateView.as_view(), name='item_service_create'), + path('items/services//update/', views.ItemServiceUpdateView.as_view(), name='item_service_update'), # Expanese path('items/expeneses/', views.ItemExpenseListView.as_view(), name='item_expense_list'), path('items/expeneses/create/', views.ItemExpenseCreateView.as_view(), name='item_expense_create'), diff --git a/inventory/utils.py b/inventory/utils.py index 8a2fb532..135450fb 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _ from inventory.utilities.financials import get_financial_value from django_ledger.models.items import ItemModel from django_ledger.models import InvoiceModel, EstimateModel +from decimal import Decimal def get_jwt_token(): diff --git a/inventory/views.py b/inventory/views.py index f4c1a98e..349f484c 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1,5 +1,6 @@ from decimal import Decimal from django.core.paginator import Paginator +from django.forms import DateField, DateInput, HiddenInput, TextInput from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django_ledger.models import ( @@ -340,19 +341,17 @@ class AjaxHandlerView(LoginRequiredMixin, View): # Validate inputs if not model_id or not year: - return JsonResponse({"error": "Missing required parameters: model_id or year"}, status=400) + return JsonResponse( + {"error": "Missing required parameters: model_id or year"}, status=400 + ) try: year = int(year) except ValueError: return JsonResponse({"error": "Invalid year format"}, status=400) series = models.CarSerie.objects.filter( - id_car_model=model_id, - year_begin__lte=year, - year_end__gte=year - ).values( - "id_car_serie", "name", "arabic_name", "generation_name" - ) + id_car_model=model_id, year_begin__lte=year, year_end__gte=year + ).values("id_car_serie", "name", "arabic_name", "generation_name") return JsonResponse(list(series), safe=False) @@ -401,15 +400,19 @@ class AjaxHandlerView(LoginRequiredMixin, View): return JsonResponse(serialized_specs, safe=False) def get_equipments(self, request): - trim_id = request.GET.get('trim_id') - equipments = models.CarEquipment.objects.filter( - id_car_trim=trim_id - ).values('id_car_equipment', 'name').order_by('name') + trim_id = request.GET.get("trim_id") + equipments = ( + models.CarEquipment.objects.filter(id_car_trim=trim_id) + .values("id_car_equipment", "name") + .order_by("name") + ) return JsonResponse(list(equipments), safe=False) def get_options(self, request): - equipment_id = request.GET.get('equipment_id') - car_option_values = models.CarOptionValue.objects.filter(id_car_equipment=equipment_id) + equipment_id = request.GET.get("equipment_id") + car_option_values = models.CarOptionValue.objects.filter( + id_car_equipment=equipment_id + ) options_by_parent = {} for value in car_option_values: @@ -418,16 +421,19 @@ class AjaxHandlerView(LoginRequiredMixin, View): parent_id = parent.id_car_option if parent else 0 parent_name = parent.name if parent else "Root" if parent_id not in options_by_parent: - options_by_parent[parent_id] = {'parent_name': parent_name, 'options': []} + options_by_parent[parent_id] = { + "parent_name": parent_name, + "options": [], + } option_data = { - 'option_id': option.id_car_option, - 'option_name': option.name, - 'is_base': value.is_base, - 'equipment_name': value.id_car_equipment.name + "option_id": option.id_car_option, + "option_name": option.name, + "is_base": value.is_base, + "equipment_name": value.id_car_equipment.name, } - options_by_parent[parent_id]['options'].append(option_data) + options_by_parent[parent_id]["options"].append(option_data) serialized_options = [ - {'parent_name': v['parent_name'], 'options': v['options']} + {"parent_name": v["parent_name"], "options": v["options"]} for v in options_by_parent.values() ] return JsonResponse(serialized_options, safe=False) @@ -863,14 +869,18 @@ class CustomerDetailView(LoginRequiredMixin, DetailView): context["estimates"] = entity.get_estimates().filter( customer__customer_name=name ) - context['notes'] = models.Notes.objects.filter(content_type__model='customer', object_id=self.object.id) - context['activities'] = models.Activity.objects.filter(content_type__model='customer', object_id=self.object.id) + context["notes"] = models.Notes.objects.filter( + content_type__model="customer", object_id=self.object.id + ) + context["activities"] = models.Activity.objects.filter( + content_type__model="customer", object_id=self.object.id + ) return context def add_note_to_customer(request, pk): customer = get_object_or_404(models.Customer, pk=pk) - if request.method == 'POST': + if request.method == "POST": form = forms.NoteForm(request.POST) if form.is_valid(): note = form.save(commit=False) @@ -878,24 +888,27 @@ def add_note_to_customer(request, pk): note.created_by = request.user note.save() - return redirect('customer_detail', pk=pk) + return redirect("customer_detail", pk=pk) else: form = forms.NoteForm() - return render(request, 'crm/add_note.html', {'form': form, 'customer': customer}) + return render(request, "crm/add_note.html", {"form": form, "customer": customer}) + def add_activity_to_customer(request, pk): customer = get_object_or_404(models.Customer, pk=pk) - if request.method == 'POST': + if request.method == "POST": form = forms.ActivityForm(request.POST) if form.is_valid(): activity = form.save(commit=False) activity.content_object = customer activity.created_by = request.user activity.save() - return redirect('customer_detail', pk=pk) + return redirect("customer_detail", pk=pk) else: form = forms.ActivityForm() - return render(request, 'crm/add_activity.html', {'form': form, 'customer': customer}) + return render( + request, "crm/add_activity.html", {"form": form, "customer": customer} + ) class CustomerCreateView( @@ -1503,6 +1516,7 @@ class RepresentativeListView(LoginRequiredMixin, ListView): data = models.Representative.objects.filter(dealer=dealer).all() return data + class RepresentativeDetailView(DetailView): model = models.Representative template_name = "representatives/representative_detail.html" @@ -1725,9 +1739,7 @@ class BankAccountListView(LoginRequiredMixin, ListView): def get_queryset(self): dealer = get_user_type(self.request) - return BankAccountModel.objects.filter( - entity_model=dealer.entity - ) + return BankAccountModel.objects.filter(entity_model=dealer.entity) class BankAccountCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): @@ -1863,6 +1875,11 @@ class AccountUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): success_url = reverse_lazy("account_list") success_message = "Account updated successfully." + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.fields['_ref_node_id'].widget = HiddenInput() + form.fields['_position'].widget = HiddenInput() + return form @login_required def account_delete(request, pk): @@ -2046,6 +2063,7 @@ def create_estimate(request): for x in car_list ], } + print(context) return render(request, "sales/estimates/estimate_form.html", context) @@ -2265,9 +2283,8 @@ def invoice_create(request, pk): dealer = get_user_type(request) entity = dealer.entity - form = InvoiceModelCreateForm(entity_slug=entity.slug, user_model=entity.admin) if request.method == "POST": - form = InvoiceModelCreateForm( + form = forms.InvoiceModelCreateForm( request.POST, entity_slug=entity.slug, user_model=entity.admin ) if form.is_valid(): @@ -2314,7 +2331,23 @@ def invoice_create(request, pk): invoice.save() messages.success(request, "Invoice created successfully!") return redirect("invoice_detail", pk=invoice.pk) - form.initial["customer"] = estimate.customer + form = forms.InvoiceModelCreateForm( + entity_slug=entity.slug, user_model=entity.admin + ) + + form.initial.update( + { + "customer": estimate.customer, + "cash_account": entity.get_default_coa_accounts().get(name="Cash"), + "prepaid_account": entity.get_default_coa_accounts().get( + name="Accounts Receivable" + ), + "unearned_account": entity.get_default_coa_accounts().get( + name="Deferred Revenue" + ), + } + ) + context = { "form": form, "estimate": estimate, @@ -2354,23 +2387,22 @@ def PaymentCreateView(request, pk=None): if form.is_valid(): amount = form.cleaned_data.get("amount") invoice = form.cleaned_data.get("invoice") - if amount > invoice.amount_due: - messages.error( - request, "Payment amount is greater than invoice amount due" - ) - return redirect("payment_create", pk=invoice.pk) - if amount <= 0: - messages.error(request, "Payment amount must be greater than 0") - return redirect("payment_create", pk=invoice.pk) - if ( - invoice.amount_due == invoice.amount_paid - or invoice.invoice_status == "paid" - ): - messages.error(request, "Invoice is already fully paid") - return redirect("invoice_detail", pk=invoice.pk) + payment_method = form.cleaned_data.get("payment_method") ledger = None try: + vat_amount = 0 + total_amount = 0 + + if invoice.terms == "on_receipt": + for x in invoice.get_itemtxs_data()[0].all(): + vat_amount += models.Car.objects.get( + vin=x.item_model.name + ).finances.vat_amount + total_amount += models.Car.objects.get( + vin=x.item_model.name + ).finances.total_discount + ledger = LedgerModel.objects.filter( name=str(invoice.pk), entity=entity ).first() @@ -2381,13 +2413,29 @@ def PaymentCreateView(request, pk=None): locked=False, origin="Payment", ) - cash_account = entity.get_default_coa_accounts().get(name="Cash") - accounts_receivable = entity.get_default_coa_accounts().get( - name="Accounts Receivable" + credit_account = entity.get_default_coa_accounts().get( + name="Sales Revenue" + ) + debit_account = None + if payment_method == "cash": + debit_account = entity.get_default_coa_accounts().get( + name="Cash", active=True + ) + elif payment_method == "credit": + debit_account = entity.get_default_coa_accounts().get( + name="Accounts Receivable", active=True + ) + else: + debit_account = entity.get_default_coa_accounts().get( + name="Cash in Bank", active=True + ) + + vat_payable_account = entity.get_default_coa_accounts().get( + name="VAT Payable", active=True ) TransactionModel.objects.create( journal_entry=journal, - account=cash_account, # Debit Cash + account=debit_account, # Debit Cash amount=amount, # Payment amount tx_type="debit", description="Payment Received", @@ -2395,27 +2443,31 @@ def PaymentCreateView(request, pk=None): TransactionModel.objects.create( journal_entry=journal, - account=accounts_receivable, # Credit Accounts Receivable - amount=amount, # Payment amount + account=credit_account, # Credit Accounts Receivable + amount=total_amount, # Payment amount tx_type="credit", description="Payment Received", ) - journal.posted = True - invoice.make_payment(amount) - journal.save() - invoice.save() - if invoice.amount_due == invoice.amount_paid: - invoice.mark_as_paid( - entity_slug=entity.slug, user_model=entity.admin + if vat_amount > 0: + TransactionModel.objects.create( + journal_entry=journal, + account=vat_payable_account, # Credit VAT Payable + amount=vat_amount, + tx_type="credit", + description="VAT Payable on Invoice", ) - invoice.save() - ledger.post() - ledger.save() - messages.success(request, "Payment created successfully!") - return redirect("invoice_detail", pk=invoice.pk) + + invoice.make_payment(amount) + invoice.save() except Exception as e: messages.error(request, f"Error creating payment: {str(e)}") + else: + messages.error(request, f"Invalid form data: {str(form.errors)}") + return redirect("invoice_detail", pk=invoice.pk) + form = forms.PaymentForm() + form.initial["amount"] = invoice.amount_due + if invoice: form.initial["invoice"] = invoice return render( @@ -2433,7 +2485,39 @@ def PaymentListView(request): def PaymentDetailView(request, pk): journal = JournalEntryModel.objects.filter(pk=pk).first() - return render(request, "sales/payments/payment_details.html", {"journal": journal}) + transactions = ( + TransactionModel.objects.filter(journal_entry=journal) + .order_by("account__code") + .all() + ) + return render( + request, + "sales/payments/payment_details.html", + {"journal": journal, "transactions": transactions}, + ) + + +def payment_mark_as_paid(request, pk): + invoice = get_object_or_404(InvoiceModel, pk=pk) + if request.method == "POST": + try: + if invoice.amount_due == invoice.amount_paid: + if not invoice.is_paid() and invoice.can_pay(): + invoice.mark_as_paid( + entity_slug=invoice.ledger.entity.slug, + user_model=invoice.ledger.entity.admin, + ) + invoice.save() + + invoice.ledger.lock_journal_entries() + invoice.ledger.post_journal_entries() + + invoice.ledger.post() + invoice.ledger.save() + messages.success(request, "Payment created successfully!") + except Exception as e: + messages.error(request, f"Error: {str(e)}") + return redirect("invoice_detail", pk=invoice.pk) # activity log @@ -2453,8 +2537,8 @@ class UserActivityLogListView(ListView): # CRM RELATED VIEWS class LeadListView(ListView): model = models.Lead - template_name = 'crm/leads/lead_list.html' - context_object_name = 'leads' + template_name = "crm/leads/lead_list.html" + context_object_name = "leads" paginate_by = 10 def get_queryset(self): @@ -2462,17 +2546,25 @@ class LeadListView(ListView): leads = models.Lead.objects.filter(dealer=dealer).all() return leads + class LeadDetailView(DetailView): model = models.Lead - template_name = 'crm/leads/lead_detail.html' + template_name = "crm/leads/lead_detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['notes'] = models.Notes.objects.filter(content_type__model='lead', object_id=self.object.id) - context['activities'] = models.Activity.objects.filter(content_type__model='lead', object_id=self.object.id) - context['status_history'] = models.LeadStatusHistory.objects.filter(lead=self.object) + context["notes"] = models.Notes.objects.filter( + content_type__model="lead", object_id=self.object.id + ) + context["activities"] = models.Activity.objects.filter( + content_type__model="lead", object_id=self.object.id + ) + context["status_history"] = models.LeadStatusHistory.objects.filter( + lead=self.object + ) return context + class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin): model = models.Lead form_class = forms.LeadForm @@ -2503,13 +2595,13 @@ class LeadUpdateView(UpdateView): class LeadDeleteView(DeleteView): model = models.Lead - template_name = 'crm/leads/lead_confirm_delete.html' - success_url = reverse_lazy('lead_list') + template_name = "crm/leads/lead_confirm_delete.html" + success_url = reverse_lazy("lead_list") def add_note_to_lead(request, pk): lead = get_object_or_404(models.Lead, pk=pk) - if request.method == 'POST': + if request.method == "POST": form = forms.NoteForm(request.POST) if form.is_valid(): note = form.save(commit=False) @@ -2517,24 +2609,25 @@ def add_note_to_lead(request, pk): note.created_by = request.user note.save() - return redirect('lead_detail', pk=pk) + return redirect("lead_detail", pk=pk) else: form = forms.NoteForm() - return render(request, 'crm/add_note.html', {'form': form, 'lead': lead}) + return render(request, "crm/add_note.html", {"form": form, "lead": lead}) + def add_activity_to_lead(request, pk): lead = get_object_or_404(models.Lead, pk=pk) - if request.method == 'POST': + if request.method == "POST": form = forms.ActivityForm(request.POST) if form.is_valid(): activity = form.save(commit=False) activity.content_object = lead activity.created_by = request.user activity.save() - return redirect('lead_detail', pk=pk) + return redirect("lead_detail", pk=pk) else: form = forms.ActivityForm() - return render(request, 'crm/add_activity.html', {'form': form, 'lead': lead}) + return render(request, "crm/add_activity.html", {"form": form, "lead": lead}) class OpportunityCreateView(CreateView): @@ -2650,11 +2743,28 @@ def fetch_notifications(request): return JsonResponse({"notifications": notifications_data}) -class ItemServiceCreateView(CreateView): +class ItemServiceCreateView(LoginRequiredMixin,SuccessMessageMixin,CreateView): model = models.AdditionalServices form_class = forms.AdditionalServiceForm template_name = "items/service/service_create.html" success_url = reverse_lazy("item_service_list") + success_message = _("Service created successfully.") + context_object_name = "service" + + def form_valid(self, form): + vat = models.VatRate.objects.get(is_active=True) + form.instance.dealer = get_user_type(self.request.user.dealer) + if form.instance.taxable: + form.instance.price = (form.instance.price * vat.rate) + form.instance.price + return super().form_valid(form) + +class ItemServiceUpdateView(LoginRequiredMixin,SuccessMessageMixin,UpdateView): + model = models.AdditionalServices + form_class = forms.AdditionalServiceForm + template_name = "items/service/service_create.html" + success_url = reverse_lazy("item_service_list") + success_message = _("Service updated successfully.") + context_object_name = "service" def form_valid(self, form): vat = models.VatRate.objects.get(is_active=True) @@ -2665,13 +2775,13 @@ class ItemServiceCreateView(CreateView): class ItemServiceListView(ListView): - model = ItemModel + model = models.AdditionalServices template_name = "items/service/service_list.html" context_object_name = "services" def get_queryset(self): dealer = get_user_type(self.request) - items = dealer.entity.get_items_services() + items = models.AdditionalServices.objects.filter(dealer=dealer).all() return items diff --git a/templates/header.html b/templates/header.html index 44b60608..87221ba3 100644 --- a/templates/header.html +++ b/templates/header.html @@ -138,7 +138,7 @@ - {% endblock content %} - diff --git a/templates/sales/payments/payment_list.html b/templates/sales/payments/payment_list.html index 59283e03..52ccc0e1 100644 --- a/templates/sales/payments/payment_list.html +++ b/templates/sales/payments/payment_list.html @@ -5,8 +5,10 @@ {% block content %}
- {% trans "Add Payment" %} -

{% trans "Payments" %}

+
+ {% trans "Add Payment" %} +

{% trans "Payments" %}

+