This commit is contained in:
gitea 2025-01-14 11:51:26 +00:00
parent 2ed8ee24eb
commit b48d404655
33 changed files with 1006 additions and 1232 deletions

BIN
db.sqlite3-shm Normal file

Binary file not shown.

0
db.sqlite3-wal Normal file
View File

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.4 on 2025-01-07 22:27
# Generated by Django 4.2.17 on 2025-01-13 08:40
from django.db import migrations, models

View File

@ -1,7 +1,7 @@
# Generated by Django 5.1.4 on 2025-01-07 22:27
# Generated by Django 4.2.17 on 2025-01-13 08:40
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 = [

View File

@ -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)
@ -640,4 +653,14 @@ class ActivityForm(forms.ModelForm):
class OpportunityForm(forms.ModelForm):
class Meta:
model = Opportunity
fields = ['customer', 'car', 'stage', 'probability', 'closing_date']
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'}))

File diff suppressed because it is too large Load Diff

View File

@ -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),
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 4.2.17 on 2025-01-08 08:42
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('django_ledger', '0017_alter_accountmodel_unique_together_and_more'),
('inventory', '0003_alter_caroptionvalue_is_base'),
]
operations = [
migrations.AddField(
model_name='additionalservices',
name='item',
field=models.OneToOneField(default='', on_delete=django.db.models.deletion.CASCADE, to='django_ledger.itemmodel', verbose_name='Item'),
preserve_default=False,
),
migrations.AlterField(
model_name='carfinance',
name='additional_services',
field=models.ManyToManyField(blank=True, related_name='additional_finances', to='inventory.additionalservices'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 4.2.17 on 2025-01-08 08:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('django_ledger', '0017_alter_accountmodel_unique_together_and_more'),
('inventory', '0004_additionalservices_item_and_more'),
]
operations = [
migrations.AlterField(
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'),
),
]

View File

@ -1,59 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-08 19:03
import django.utils.timezone
import django_countries.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_alter_additionalservices_item'),
]
operations = [
migrations.RemoveField(
model_name='customer',
name='is_lead',
),
migrations.AddField(
model_name='customer',
name='dob',
field=models.DateField(default=django.utils.timezone.now, verbose_name='Date of Birth'),
preserve_default=False,
),
migrations.AddField(
model_name='customer',
name='gender',
field=models.CharField(choices=[('m', 'Male'), ('f', 'Female')], default='m', max_length=1, verbose_name='Gender'),
preserve_default=False,
),
migrations.AddField(
model_name='customer',
name='nationality',
field=django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Nationality'),
),
migrations.AddField(
model_name='customer',
name='obligations',
field=models.PositiveIntegerField(default=1000, verbose_name='Obligations'),
preserve_default=False,
),
migrations.AddField(
model_name='customer',
name='salary',
field=models.PositiveIntegerField(default=10000, verbose_name='Salary'),
preserve_default=False,
),
migrations.AddField(
model_name='customer',
name='title',
field=models.CharField(choices=[('mr', 'Mr'), ('mrs', 'Mrs'), ('ms', 'Ms'), ('miss', 'Miss'), ('dr', 'Dr'), ('prof', 'Prof'), ('prince', 'Prince'), ('princess', 'Princess'), ('company', 'Company')], default='mr', max_length=10, verbose_name='Title'),
preserve_default=False,
),
migrations.AddField(
model_name='customer',
name='updated',
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
),
]

View File

@ -1,43 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-09 05:46
import django_countries.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_remove_customer_is_lead_customer_dob_customer_gender_and_more'),
]
operations = [
migrations.RemoveField(
model_name='customer',
name='nationality',
),
migrations.AddField(
model_name='customer',
name='country',
field=django_countries.fields.CountryField(blank=True, max_length=2, verbose_name='Country'),
),
migrations.AlterField(
model_name='customer',
name='title',
field=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'),
),
migrations.AlterField(
model_name='opportunity',
name='deal_status',
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], default='new', max_length=20, verbose_name='Deal Status'),
),
migrations.AlterField(
model_name='opportunitylog',
name='new_status',
field=models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], max_length=50, null=True, verbose_name='New Status'),
),
migrations.AlterField(
model_name='opportunitylog',
name='old_status',
field=models.CharField(blank=True, choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('canceled', 'Canceled'), ('lost', 'Lost'), ('won', 'Won')], max_length=50, null=True, verbose_name='Old Status'),
),
]

View File

@ -1,224 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-09 09:19
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('inventory', '0007_remove_customer_nationality_customer_country_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='notes',
options={'verbose_name': 'Note', 'verbose_name_plural': 'Notes'},
),
migrations.AlterModelOptions(
name='notification',
options={'ordering': ['-created'], 'verbose_name': 'Notification', 'verbose_name_plural': 'Notifications'},
),
migrations.RemoveField(
model_name='customer',
name='obligations',
),
migrations.RemoveField(
model_name='customer',
name='salary',
),
migrations.RemoveField(
model_name='notes',
name='created_at',
),
migrations.RemoveField(
model_name='notes',
name='opportunity',
),
migrations.RemoveField(
model_name='notes',
name='updated_at',
),
migrations.RemoveField(
model_name='notification',
name='created_at',
),
migrations.RemoveField(
model_name='opportunity',
name='created_at',
),
migrations.RemoveField(
model_name='opportunity',
name='created_by',
),
migrations.RemoveField(
model_name='opportunity',
name='deal_name',
),
migrations.RemoveField(
model_name='opportunity',
name='deal_status',
),
migrations.RemoveField(
model_name='opportunity',
name='deal_value',
),
migrations.RemoveField(
model_name='opportunity',
name='priority',
),
migrations.RemoveField(
model_name='opportunity',
name='updated_at',
),
migrations.RemoveField(
model_name='staff',
name='created_at',
),
migrations.RemoveField(
model_name='staff',
name='updated_at',
),
migrations.AddField(
model_name='notes',
name='content_type',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
preserve_default=False,
),
migrations.AddField(
model_name='notes',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
preserve_default=False,
),
migrations.AddField(
model_name='notes',
name='object_id',
field=models.PositiveIntegerField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name='notes',
name='updated',
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
),
migrations.AddField(
model_name='notification',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
preserve_default=False,
),
migrations.AddField(
model_name='opportunity',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
preserve_default=False,
),
migrations.AddField(
model_name='opportunity',
name='dealer',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.dealer'),
preserve_default=False,
),
migrations.AddField(
model_name='opportunity',
name='staff',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to='inventory.staff', verbose_name='Owner'),
),
migrations.AddField(
model_name='opportunity',
name='stage',
field=models.CharField(choices=[('prospect', 'Prospect'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], default='prospect', max_length=20, verbose_name='Stage'),
preserve_default=False,
),
migrations.AddField(
model_name='opportunity',
name='updated',
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
),
migrations.AddField(
model_name='staff',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created'),
preserve_default=False,
),
migrations.AddField(
model_name='staff',
name='updated',
field=models.DateTimeField(auto_now=True, verbose_name='Updated'),
),
migrations.AlterField(
model_name='notes',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to='inventory.staff'),
),
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='inventory.staff')),
],
options={
'verbose_name': 'Activity',
'verbose_name_plural': 'Activities',
},
),
migrations.CreateModel(
name='Lead',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')], max_length=20, verbose_name='Title')),
('first_name', models.CharField(max_length=50, verbose_name='First Name')),
('last_name', models.CharField(max_length=50, verbose_name='Last Name')),
('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='Email')),
('salary', models.PositiveIntegerField(blank=True, null=True, verbose_name='Salary')),
('obligations', models.PositiveIntegerField(blank=True, null=True, verbose_name='Obligations')),
('year', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Year')),
('source', models.CharField(choices=[('referrals', 'Referrals'), ('whatsapp', 'WhatsApp'), ('showroom', 'Showroom'), ('tiktok', 'TikTok'), ('instagram', 'Instagram'), ('x', 'X'), ('facebook', 'Facebook'), ('motory', 'Motory'), ('influencers', 'Influencers'), ('youtube', 'Youtube'), ('campaign', 'Campaign')], max_length=50, verbose_name='Source')),
('channel', models.CharField(choices=[('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('website', 'Website'), ('email', 'Email'), ('form', 'Form')], max_length=50, verbose_name='Channel')),
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='low', max_length=10, verbose_name='Priority')),
('status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], db_index=True, max_length=50, verbose_name='Status')),
('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('assigned', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned', to='inventory.staff', verbose_name='Assigned')),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leads', to='inventory.dealer')),
('id_car_make', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmake', verbose_name='Make')),
('id_car_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='inventory.carmodel', verbose_name='Model')),
],
options={
'verbose_name': 'Lead',
'verbose_name_plural': 'Leads',
},
),
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.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'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status')),
('new_status', models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('contacted', 'Contacted'), ('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.DeleteModel(
name='OpportunityLog',
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-09 09:57
import inventory.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0008_alter_notes_options_alter_notification_options_and_more'),
]
operations = [
migrations.AlterModelManagers(
name='staff',
managers=[
('objects', inventory.models.StaffUserManager()),
],
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-09 11:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0009_alter_staff_managers'),
]
operations = [
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'),
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-09 20:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0010_customer_staff'),
]
operations = [
migrations.RemoveField(
model_name='customer',
name='country',
),
migrations.AddField(
model_name='customer',
name='city',
field=models.CharField(blank=True, max_length=255, verbose_name='City'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-11 10:32
import inventory.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0011_remove_customer_country_customer_city'),
]
operations = [
migrations.AddField(
model_name='opportunity',
name='probability',
field=models.PositiveIntegerField(default=70, validators=[inventory.models.validate_probability]),
preserve_default=False,
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-11 11:09
import phonenumber_field.modelfields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0012_opportunity_probability'),
]
operations = [
migrations.AddField(
model_name='lead',
name='phone_number',
field=phonenumber_field.modelfields.PhoneNumberField(default='0535521547', max_length=128, region='SA', verbose_name='Phone Number'),
preserve_default=False,
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-11 12:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0013_lead_phone_number'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='activity',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='activities_created', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='notes',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='notes_created', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-11 12:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0014_alter_activity_created_by_alter_notes_created_by'),
]
operations = [
migrations.AddField(
model_name='lead',
name='city',
field=models.CharField(default='Riyadh', max_length=50, verbose_name='City'),
preserve_default=False,
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-11 12:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0015_lead_city'),
]
operations = [
migrations.AddField(
model_name='lead',
name='address',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Address'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-11 19:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0016_lead_address'),
]
operations = [
migrations.AlterField(
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'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-11 23:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0017_alter_lead_assigned'),
]
operations = [
migrations.AlterField(
model_name='lead',
name='priority',
field=models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=10, verbose_name='Priority'),
),
]

View File

@ -1,45 +0,0 @@
# Generated by Django 5.1.4 on 2025-01-12 01:43
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0018_alter_lead_priority'),
]
operations = [
migrations.AddField(
model_name='opportunity',
name='closed',
field=models.BooleanField(default=False, verbose_name='Closed'),
),
migrations.AddField(
model_name='opportunity',
name='closing_date',
field=models.DateField(default=django.utils.timezone.now, verbose_name='Closing Date'),
preserve_default=False,
),
migrations.AddField(
model_name='opportunity',
name='status',
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], default='new', max_length=20, verbose_name='Status'),
),
migrations.AlterField(
model_name='lead',
name='status',
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], db_index=True, default='new', max_length=50, verbose_name='Status'),
),
migrations.AlterField(
model_name='leadstatushistory',
name='new_status',
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='New Status'),
),
migrations.AlterField(
model_name='leadstatushistory',
name='old_status',
field=models.CharField(choices=[('new', 'New'), ('pending', 'Pending'), ('in_progress', 'In Progress'), ('qualified', 'Qualified'), ('canceled', 'Canceled')], max_length=50, verbose_name='Old Status'),
),
]

View File

@ -48,6 +48,8 @@ class StaffUserManager(UserManager):
Staff.objects.create(user=user, name=name, arabic_name=arabic_name, phone_number=phone_number, staff_type=staff_type, **extra_fields)
return user
class CarType(models.TextChoices):
pass
class UnitOfMeasure(models.TextChoices):
EACH = 'EA', 'Each'
@ -82,7 +84,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

View File

@ -128,23 +128,11 @@ def create_ledger_entity(sender, instance, created, **kwargs):
)
asset_ca_receivables.role_default = True
asset_ca_receivables.save()
# Inventory Account
asset_ca_inventory = entity.create_account(
coa_model=coa,
code="1104",
role=roles.ASSET_CA_INVENTORY,
name=_("Inventory"),
balance_type="debit",
active=True,
)
asset_ca_inventory.role_default = True
asset_ca_inventory.save()
# Prepaid Expenses Account
asset_ca_prepaid = entity.create_account(
coa_model=coa,
code="1105",
code="1104",
role=roles.ASSET_CA_PREPAID,
name=_("Prepaid Expenses"),
balance_type="debit",
@ -152,44 +140,55 @@ def create_ledger_entity(sender, instance, created, **kwargs):
)
asset_ca_prepaid.role_default = True
asset_ca_prepaid.save()
# Prepaid Expenses Account
# Employee Expenses Account
asset_ca_prepaid_employee = entity.create_account(
coa_model=coa,
code="1106",
code="1105",
role=roles.ASSET_CA_PREPAID,
name=_("Employee Advance"),
balance_type="debit",
active=True,
)
# Notes Receivable Account
asset_lti_notes_receivable = entity.create_account(
# 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(
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()
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="1203",
code="1202",
role=roles.ASSET_INTANGIBLE_ASSETS,
name=_("Intangible Assets"),
balance_type="debit",
@ -198,7 +197,7 @@ def create_ledger_entity(sender, instance, created, **kwargs):
asset_lti_land_intangable.role_default = True
asset_lti_land_intangable.save()
# investment Account
# investment property Account
asset_lti_land_investment = entity.create_account(
coa_model=coa,
code="1204",
@ -209,7 +208,32 @@ def create_ledger_entity(sender, instance, created, **kwargs):
)
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,
@ -222,17 +246,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()
asset_ppe_buildings_accum_depreciation = entity.create_account(
coa_model=coa,
code="1303",
@ -308,17 +322,17 @@ def create_ledger_entity(sender, instance, created, **kwargs):
liability_ltl_vat_payable = entity.create_account(
coa_model=coa,
code="2106",
role=roles.LIABILITY_LTL_BONDS_PAYABLE,
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_LTL_NOTES_PAYABLE,
role=roles.LIABILITY_CL_OTHER,
name=_("Taxes Payable"),
balance_type="credit",
active=True,
@ -334,6 +348,9 @@ def create_ledger_entity(sender, instance, created, **kwargs):
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,
@ -346,29 +363,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(
@ -404,6 +435,12 @@ def create_ledger_entity(sender, instance, created, **kwargs):
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(
@ -416,12 +453,7 @@ def create_ledger_entity(sender, instance, created, **kwargs):
)
expense_cogs.role_default = True
expense_cogs.save()
# 5.1 Direct Costs
entity.create_account(coa_model=coa, code="5101", role=roles.EXPENSE_OPERATIONAL, name=_("Cost of Goods Sold"), balance_type="debit", active=True)
entity.create_account(coa_model=coa, code="5102", role=roles.EXPENSE_OPERATIONAL, name=_("Salaries and Wages"), balance_type="debit", active=True)
entity.create_account(coa_model=coa, code="5103", role=roles.EXPENSE_OPERATIONAL, name=_("Sales Commissions"), balance_type="debit", active=True)
entity.create_account(coa_model=coa, code="5104", role=roles.EXPENSE_OPERATIONAL, name=_("Shipping and Customs Clearance"), balance_type="debit", active=True)
# accrued Expenses Account
expense_cogs = entity.create_account(
@ -595,12 +627,18 @@ def create_ledger_entity(sender, instance, created, **kwargs):
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

View File

@ -162,6 +162,7 @@ urlpatterns = [
path('sales/payments/<uuid:pk>/create/', views.PaymentCreateView, name='payment_create'),
path('sales/payments/create/', views.PaymentCreateView, name='payment_create'),
path('sales/payments/<uuid:pk>/payment_details/', views.PaymentDetailView, name='payment_details'),
path('sales/payments/<uuid:pk>/payment_mark_as_paid/', views.payment_mark_as_paid, name='payment_mark_as_paid'),
# path('sales/payments/<uuid:pk>/update/', views.JournalEntryUpdateView.as_view(), name='payment_update'),
# path('sales/payments/<uuid:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='payment_delete'),
# path('sales/payments/<uuid:pk>/preview/', views.JournalEntryPreviewView.as_view(), name='payment_preview'),

View File

@ -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():

View File

@ -1,5 +1,6 @@
from decimal import Decimal
from django.core.paginator import Paginator
from django.forms import DateField, DateInput, HiddenInput, TextInput
from django_ledger.models import (
EntityModel,
InvoiceModel,
@ -334,19 +335,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)
@ -395,15 +394,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:
@ -412,16 +415,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)
@ -827,14 +833,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)
@ -842,24 +852,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(
@ -1467,6 +1480,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"
@ -1689,9 +1703,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):
@ -1807,7 +1819,12 @@ class AccountUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
template_name = "ledger/coa_accounts/account_form.html"
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):
@ -1990,6 +2007,7 @@ def create_estimate(request):
for x in car_list
],
}
print(context)
return render(request, "sales/estimates/estimate_form.html", context)
@ -2208,9 +2226,8 @@ def invoice_create(request, pk):
estimate = get_object_or_404(EstimateModel, pk=pk)
entity = request.user.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():
@ -2257,7 +2274,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,
@ -2289,30 +2322,29 @@ class InvoicePreviewView(LoginRequiredMixin, DetailView):
def PaymentCreateView(request, pk=None):
invoice = InvoiceModel.objects.filter(pk=pk).first()
entity = request.user.dealer.entity
form = forms.PaymentForm()
if request.method == "POST":
form = forms.PaymentForm(request.POST)
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()
@ -2323,13 +2355,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",
@ -2337,27 +2385,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(
@ -2375,7 +2427,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
@ -2395,8 +2479,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):
@ -2404,24 +2488,31 @@ 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.LeadCreateForm
template_name = 'crm/leads/lead_create_form.html'
template_name = "crm/leads/lead_create_form.html"
success_message = "Lead created successfully!"
success_url = reverse_lazy('lead_list')
success_url = reverse_lazy("lead_list")
def form_valid(self, form):
print("Form data:", form.cleaned_data) # Debug form data
@ -2433,18 +2524,19 @@ class LeadCreateView(CreateView, SuccessMessageMixin, LoginRequiredMixin):
class LeadUpdateView(UpdateView):
model = models.Lead
form_class = forms.LeadUpdateForm
template_name = 'crm/leads/lead_update_form.html'
success_url = reverse_lazy('lead_list')
template_name = "crm/leads/lead_update_form.html"
success_url = reverse_lazy("lead_list")
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)
@ -2452,24 +2544,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):

View File

@ -138,7 +138,7 @@
</ul>
</div>
</div>
<div class="nav-item-wrapper">
<div class="nav-item-wrapper">
<a class="nav-link dropdown-indicator label-1" href="#nv-crm" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-sales">
<div class="d-flex align-items-center">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
@ -190,6 +190,58 @@
</div>
</div>
<!--Add Above -->
<div class="nav-item-wrapper">
<a class="nav-link dropdown-indicator label-1" href="#nv-financial" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-financial">
<div class="d-flex align-items-center">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
<span class="nav-link-icon"><span data-feather="phone"></span></span><span class="nav-link-text">{% trans 'crm'|upper %}</span>
</div>
</a>
<div class="parent-wrapper label-1">
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-financial">
<li class="collapsed-nav-item-title d-none">{% trans 'crm'|upper %}</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'account_list' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span data-feather="users"></span></span><span class="nav-link-text">{% trans 'Accounts'|capfirst %}</span>
</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'bank_account_list' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span data-feather="users"></span></span><span class="nav-link-text">{% trans 'Bank Accounts'|capfirst %}</span>
</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'item_service_list' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span data-feather="activity"></span></span><span class="nav-link-text">{% trans "Services"|capfirst %}</span>
</div>
</a>
<!-- more inner pages-->
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'item_expense_list' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-users-cog"></span></span><span class="nav-link-text">{% trans "Expenses"|capfirst %}</span>
</div>
</a>
<!-- more inner pages-->
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'opportunity_list' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-money-check"></span></span><span class="nav-link-text">{% trans "Opportunities"|capfirst %}</span>
</div>
</a>
<!-- more inner pages-->
</li>
</ul>
</div>
</div>
<!--Add Above -->
</li>
</ul>
</div>

View File

@ -38,7 +38,7 @@
<!-- Buttons -->
<div class="mt-5 text-center">
<button type="submit" class="btn btn-success me-2">{% trans "Save" %}</button>
<button type="submit" class="btn btn-success me-2" {% if not items %}disabled{% endif %}>{% trans "Save" %}</button>
<a href="{% url 'estimate_list' %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
</div>
</form>

View File

@ -27,6 +27,31 @@
</div>
</div>
</div>
</div>
<!-- ============================================-->
<div class="modal fade" id="mark_as_paid_Modal" tabindex="-1" aria-labelledby="confirmModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-primary">
<h5 class="modal-title text-light" id="confirmModalLabel">{% trans 'Confirm' %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% trans 'Are you sure' %}
<div class="modal-footer">
<button type="button"
class="btn btn-sm btn-danger"
data-bs-dismiss="modal">
{% trans 'No' %}
</button>
<form id="confirmForm" method="POST" action="{% url 'payment_mark_as_paid' invoice.pk %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-success btn-sm">{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- ============================================-->
<!-- <section> begin ============================-->
@ -39,8 +64,11 @@
<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">{% trans 'Accept' %}</span></button>
{% endif %}
{% if invoice.invoice_status == 'approved' %}
<a href="{% url 'payment_create' invoice.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{% trans 'Record Payment' %}</span></a>
<a href="{% url 'payment_create' invoice.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{% trans 'Record Payment' %}</span></a>
{% endif %}
{% if not invoice.is_paid %}
<button id="mark_invoice_as_paid" class="btn btn-phoenix-secondary" data-bs-toggle="modal" data-bs-target="#mark_as_paid_Modal"><span class="d-none d-sm-inline-block">{% trans 'Mark as Paid' %}</span></button>
{% endif %}
<a href="{% url 'invoice_preview' invoice.pk %}" class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{% trans 'Preview' %}</span></a>
</div>
</div>
@ -54,20 +82,63 @@
<div class="d-flex bg-success-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-success-dark" data-feather="dollar-sign" style="width:24px; height:24px"></span></div>
<div>
<p class="fw-bold mb-1">{% trans 'Paid Amount' %}</p>
<h4 class="fw-bolder text-nowrap">${{invoice.amount_paid}}</h4>
<h4 class="fw-bolder text-nowrap {% if invoice.is_paid %}text-success{% endif %}">${{invoice.amount_paid}}</h4>
<h6 class="fw-bolder text-nowrap">Owned <span class="fw-semibold text-nowrap text-success">${{invoice.get_amount_open}}</span></h6>
<div class="progress" style="height:17px">
<div class="progress-bar fw-semibold bg-{% if invoice.get_progress_percent < 100 %}secondary{% else %}success{% endif %} rounded-2" role="progressbar" style="width: {{invoice.get_progress_percent}}%"" aria-valuenow="{{invoice.get_progress_percent}}" aria-valuemin="0" aria-valuemax="100">{{invoice.get_progress_percent}}%</div>
<div class="progress-bar fw-semibold bg-{% if invoice.get_progress_percent < 100 %}secondary{% else %}success{% endif %} rounded-2" role="progressbar" style="width: {{invoice.get_progress_percent}}%" aria-valuenow="{{invoice.get_progress_percent}}" aria-valuemin="0" aria-valuemax="100">{{invoice.get_progress_percent}}%</div>
</div>
</div>
</div>
</div>
{% if 'net' in invoice.terms %}
<div class="col-sm-auto">
<div class="d-sm-block d-inline-flex d-md-flex flex-xl-column flex-xxl-row align-items-center align-items-xl-start align-items-xxl-center border-start-sm ps-sm-5 border-translucent">
<div class="d-flex bg-primary-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-primary-dark" data-feather="layout" style="width:24px; height:24px"></span></div>
<div>
<p class="fw-bold mb-1"></p>
<div class="fs-9 text-body-secondary fw-semibold mb-0 d-sm-block d-inline-flex d-md-flex flex-xl-column ">
<table>
<tr>
<td>{% trans 'Terms' %}:</td>
<td><span class="badge rounded bg-success">{{invoice.terms}}</span></td>
</tr>
<tr>
<td>{% trans 'Date Due' %}:</td>
<td><span class="badge rounded bg-success">{{invoice.date_due}}</span></td>
</tr>
<tr>
<td>{% trans 'Due in Days' %}:</td>
<td>
<span class="badge rounded bg-success">{{invoice.due_in_days}}</span>
</td>
</tr>
<tr>
<td>{% trans 'Is Past Due' %}:</td>
<td>
{% if invoice.is_past_due %}
<span class="badge rounded bg-danger">{% trans 'Yes' %}</span>
{% else %}
<span class="badge rounded bg-success">{% trans 'No' %}</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="col-sm-auto">
<div class="d-sm-block d-inline-flex d-md-flex flex-xl-column flex-xxl-row align-items-center align-items-xl-start align-items-xxl-center border-start-sm ps-sm-5 border-translucent">
<div class="d-flex bg-primary-subtle rounded flex-center me-3 mb-sm-3 mb-md-0 mb-xl-3 mb-xxl-0" style="width:32px; height:32px"><span class="text-primary-dark" data-feather="layout" style="width:24px; height:24px"></span></div>
<div>
<p class="fw-bold mb-1">{% trans 'Due Amount' %}</p>
<h4 class="fw-bolder text-nowrap">${{invoice.amount_due}} </h4>
{% if invoice.is_paid %}
<s><h4 class="fw-bolder text-nowrap">${{invoice.amount_due}} </h4></s>
{% else %}
<h4 class="fw-bolder text-nowrap">${{invoice.amount_due}} </h4>
{% endif %}
</div>
</div>
</div>
@ -75,7 +146,7 @@
</div>
</div>
<!-- <section> begin ============================-->
<div class="bg-body dark__bg-gray-1100 p-4 mb-4 rounded-2">
<div class="bg-body dark__bg-gray-1100 p-4 mb-4 rounded-2 text-body-tertiary">
<div class="row g-4">
<div class="col-12 col-lg-3">
<div class="row g-4 g-lg-2">
@ -188,7 +259,7 @@
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}

View File

@ -16,21 +16,21 @@
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Timestamp" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Account Name" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Account Code" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Description" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Credit" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Debit" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Credit" %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Description" %}</th>
</thead>
<tbody class="list">
{% for transaction in journal.transactionmodel_set.all %}
{% for transaction in transactions %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td>{{ forloop.counter }}</td>
<td class="align-middle product white-space-nowrap py-0">{{ transaction.created|date}}</td>
<td class="align-middle product white-space-nowrap">{{ transaction.account.name }}</td>
<td class="align-middle product white-space-nowrap">{{ transaction.account.name }}</td>
<td class="align-middle product white-space-nowrap">{{ transaction.account.code }}</td>
<td class="align-middle product white-space-nowrap">{{ transaction.description }}</td>
<td class="align-middle product white-space-nowrap">{% if transaction.tx_type == "credit" %}${{ transaction.amount }}{% endif %}</td>
<td class="align-middle product white-space-nowrap">{% if transaction.tx_type == "debit" %}${{ transaction.amount }}{% endif %}</td>
<td class="align-middle product white-space-nowrap">{% if transaction.tx_type == "credit" %}${{ transaction.amount }}{% endif %}</td>
<td class="align-middle product white-space-nowrap">{{ transaction.description }}</td>
</tr>
{% empty %}
<tr>

View File

@ -37,4 +37,3 @@
</div>
</div>
{% endblock content %}