From 8cd795c23738445ceda3b5c69b8ef06f6ca64b93 Mon Sep 17 00:00:00 2001 From: gitea Date: Thu, 13 Feb 2025 08:49:51 +0000 Subject: [PATCH 1/2] update --- templates/crm/opportunities/opportunity_detail.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/crm/opportunities/opportunity_detail.html b/templates/crm/opportunities/opportunity_detail.html index e7dd3f31..5a72fc90 100644 --- a/templates/crm/opportunities/opportunity_detail.html +++ b/templates/crm/opportunities/opportunity_detail.html @@ -32,10 +32,11 @@
-
+
{% if opportunity.car %}

{{ opportunity.car.id_car_make.get_local_name }} - {{ opportunity.car.id_car_model.get_local_name }} - {{ opportunity.car.year }}

- {% endif %} + {% endif %} +

{{ opportunity.customer.customer_name }}

{% if opportunity.car.finances %}
{{ opportunity.car.finances.total }} {{ _("SAR") }}
From 771390045b7dcf6294f606aa1bc1ca634bb95b32 Mon Sep 17 00:00:00 2001 From: gitea Date: Thu, 13 Feb 2025 14:21:18 +0000 Subject: [PATCH 2/2] update --- inventory/migrations/0023_email.py | 36 +++ .../0024_remove_email_body_email_message.py | 22 ++ inventory/migrations/0025_email_status.py | 18 ++ inventory/models.py | 28 ++ inventory/urls.py | 10 + inventory/views.py | 62 ++++- templates/crm/leads/lead_detail.html | 258 ++---------------- templates/crm/leads/lead_send.html | 12 +- .../crm/opportunities/opportunity_detail.html | 137 +++------- templates/customers/view_customer.html | 16 +- 10 files changed, 229 insertions(+), 370 deletions(-) create mode 100644 inventory/migrations/0023_email.py create mode 100644 inventory/migrations/0024_remove_email_body_email_message.py create mode 100644 inventory/migrations/0025_email_status.py diff --git a/inventory/migrations/0023_email.py b/inventory/migrations/0023_email.py new file mode 100644 index 00000000..500a86b4 --- /dev/null +++ b/inventory/migrations/0023_email.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.17 on 2025-02-13 09:01 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0002_remove_content_type_name'), + ('inventory', '0022_opportunity_estimate'), + ] + + operations = [ + migrations.CreateModel( + name='Email', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('from_email', models.TextField(blank=True, null=True, verbose_name='From Email')), + ('to_email', models.TextField(blank=True, null=True, verbose_name='To Email')), + ('subject', models.TextField(blank=True, null=True, verbose_name='Subject')), + ('body', models.TextField(blank=True, null=True, verbose_name='Body')), + ('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='emails_created', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Email', + 'verbose_name_plural': 'Emails', + }, + ), + ] diff --git a/inventory/migrations/0024_remove_email_body_email_message.py b/inventory/migrations/0024_remove_email_body_email_message.py new file mode 100644 index 00000000..0005c07c --- /dev/null +++ b/inventory/migrations/0024_remove_email_body_email_message.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.17 on 2025-02-13 09:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0023_email'), + ] + + operations = [ + migrations.RemoveField( + model_name='email', + name='body', + ), + migrations.AddField( + model_name='email', + name='message', + field=models.TextField(blank=True, null=True, verbose_name='Message'), + ), + ] diff --git a/inventory/migrations/0025_email_status.py b/inventory/migrations/0025_email_status.py new file mode 100644 index 00000000..baca6d3c --- /dev/null +++ b/inventory/migrations/0025_email_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2025-02-13 09:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0024_remove_email_body_email_message'), + ] + + operations = [ + migrations.AddField( + model_name='email', + name='status', + field=models.CharField(choices=[('SENT', 'Sent'), ('FAILED', 'Failed'), ('DELIVERED', 'Delivered'), ('OPEN', 'Open'), ('DRAFT', 'Draft')], default='OPEN', max_length=20, verbose_name='Status'), + ), + ] diff --git a/inventory/models.py b/inventory/models.py index d05bb82b..d1e81d43 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -71,6 +71,12 @@ class StaffUserManager(UserManager): ) return user +class EmailStatus(models.TextChoices): + SENT = "SENT", "Sent" + FAILED = "FAILED", "Failed" + DELIVERED = "DELIVERED", "Delivered" + OPEN = "OPEN", "Open" + DRAFT = "DRAFT", "Draft" class UnitOfMeasure(models.TextChoices): EACH = "EA", "Each" @@ -1346,6 +1352,28 @@ class Notes(models.Model): def __str__(self): return f"Note by {self.created_by.first_name} on {self.content_object}" +class Email(models.Model): + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey("content_type", "object_id") + from_email = models.TextField(verbose_name=_("From Email"),null=True,blank=True) + to_email = models.TextField(verbose_name=_("To Email"),null=True,blank=True) + subject = models.TextField(verbose_name=_("Subject"),null=True,blank=True) + message = models.TextField(verbose_name=_("Message"),null=True,blank=True) + status = models.CharField(max_length=20, choices=EmailStatus.choices, verbose_name=_("Status"),default=EmailStatus.OPEN) + created_by = models.ForeignKey( + User, on_delete=models.DO_NOTHING, related_name="emails_created" + ) + created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) + updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) + + class Meta: + verbose_name = _("Email") + verbose_name_plural = _("Emails") + + def __str__(self): + return f"Email by {self.created_by.first_name} on {self.content_object}" + class Activity(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) diff --git a/inventory/urls.py b/inventory/urls.py index 5c6e2cf4..e1e894a6 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -107,12 +107,22 @@ urlpatterns = [ views.send_lead_email, name="send_lead_email", ), + path( + "crm/leads//send_lead_email/", + views.send_lead_email, + name="send_lead_email_with_template", + ), path( "crm/leads//schedule/", views.schedule_lead, name="schedule_lead", ), + path( + "crm/opportunities//add_note/", + views.add_note_to_opportunity, + name="add_note_to_opportunity", + ), path( "crm/opportunities/create/", views.OpportunityCreateView.as_view(), diff --git a/inventory/views.py b/inventory/views.py index 44401c15..30561ffa 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1234,7 +1234,10 @@ class CustomerDetailView(LoginRequiredMixin, DetailView): # context["estimates"] = entity.get_estimates().filter( # customer__customer_name=name # ) - context["estimates"] = context["customer"].estimatemodel_set.all() + context["estimates"] = entity.get_estimates().filter(customer=self.object) + context["invoices"] = entity.get_invoices().filter(customer=self.object) + + # context["notes"] = models.Notes.objects.filter( # content_type__model="customer", object_id=self.object.id # ) @@ -2590,7 +2593,7 @@ def create_estimate(request,pk=None): } for x in car_list ], - "opportunity_id": opportunity_id + "opportunity_id": pk if pk else None } return render(request, "sales/estimates/estimate_form.html", context) @@ -3089,6 +3092,13 @@ class LeadDetailView(DetailView): context["notes"] = models.Notes.objects.filter( content_type__model="lead", object_id=self.object.id ) + email_qs = models.Email.objects.filter( + content_type__model="lead", object_id=self.object.id + ) + context["emails"] = { + "sent": email_qs.filter(status="SENT"), + "draft": email_qs.filter(status="DRAFT"), + } context["activities"] = models.Activity.objects.filter( content_type__model="lead", object_id=self.object.id ) @@ -3155,6 +3165,18 @@ def add_note_to_lead(request, pk): form = forms.NoteForm() return render(request, "crm/note_form.html", {"form": form, "lead": lead}) +@login_required +def add_note_to_opportunity(request, pk): + opportunity = get_object_or_404(models.Opportunity, pk=pk) + if request.method == "POST": + notes = request.POST.get("notes") + if not notes: + messages.error(request, "Notes field is required.") + else: + models.Notes.objects.create(content_object=opportunity, created_by=request.user,note=notes) + messages.success(request, "Note added successfully!") + return redirect("opportunity_detail", pk=opportunity.pk) + @login_required def update_note(request, pk): @@ -3244,21 +3266,38 @@ def schedule_lead(request, pk): @login_required -def send_lead_email(request, pk): +def send_lead_email(request, pk,email_pk=None): lead = get_object_or_404(models.Lead, pk=pk) + status = request.GET.get("status") + if status == 'draft': + models.Email.objects.create(content_object=lead, created_by=request.user,from_email="manager@tenhal.com", to_email=request.GET.get("to"), subject=request.GET.get("subject"), message=request.GET.get("message"),status=models.EmailStatus.DRAFT) + models.Activity.objects.create(content_object=lead, notes="Email Draft",created_by=request.user,activity_type=models.ActionChoices.EMAIL) + messages.success(request, _("Email Draft successfully!")) + response = HttpResponse(redirect("lead_detail", pk=lead.pk)) + response['HX-Redirect'] = reverse('lead_detail', args=[lead.pk]) + return response + dealer = get_user_type(request) lead.status = models.Status.IN_PROGRESS lead.save() # lead.convert_to_customer(dealer.entity) - if request.method == "POST": + if request.method == "POST": + email_pk = request.POST.get('email_pk') + if email_pk != "None" or email_pk != "": + email = get_object_or_404(models.Email, pk=int(email_pk)) + email.status = models.EmailStatus.SENT + email.save() + else: + models.Email.objects.create(content_object=lead, created_by=request.user,from_email="manager@tenhal.com", to_email=request.POST.get("to"), subject=request.POST.get("subject"), message=request.POST.get("message"),status=models.EmailStatus.SENT) send_email( "manager@tenhal.com", request.POST.get("to"), request.POST.get("subject"), request.POST.get("message"), ) - messages.success(request, _("Email sent successfully!")) + models.Activity.objects.create(content_object=lead, notes="Email sent",created_by=request.user,activity_type=models.ActionChoices.EMAIL) + messages.success(request, _("Email sent successfully!")) return redirect("lead_list") msg = f""" السلام عليكم @@ -3282,10 +3321,15 @@ def send_lead_email(request, pk): [Your Company] [Your Contact Information] """ + subject = "" + if email_pk: + email = get_object_or_404(models.Email, pk=email_pk) + msg = email.message + subject = email.subject return render( request, "crm/leads/lead_send.html", - {"lead": lead, "message": msg}, + {"lead": lead, "message": msg,"subject":subject, "email_pk": email_pk}, ) @@ -3295,7 +3339,7 @@ def add_activity_to_lead(request, pk): if request.method == "POST": form = forms.ActivityForm(request.POST) if form.is_valid(): - activity = form.save(commit=False) + activity = form.save(commit=False) activity.content_object = lead activity.created_by = request.user activity.save() @@ -3358,7 +3402,9 @@ class OpportunityDetailView(DetailView): form.fields["stage"].widget.attrs["hx-get"] = url form.fields["status"].initial = self.object.status form.fields["stage"].initial = self.object.stage - context["status_form"] = form + context["status_form"] = form + context["notes"] = models.Notes.objects.filter(content_type__model="opportunity", object_id=self.object.id) + context["activities"] = models.Activity.objects.filter(content_type__model="opportunity", object_id=self.object.id) return context diff --git a/templates/crm/leads/lead_detail.html b/templates/crm/leads/lead_detail.html index cf2239e7..57228dcf 100644 --- a/templates/crm/leads/lead_detail.html +++ b/templates/crm/leads/lead_detail.html @@ -236,8 +236,8 @@
@@ -261,104 +261,22 @@ + {% for email in emails.sent %}
- Quary about purchased soccer socks -
jackson@mail.com
+ {{email.subject}} +
{{email.to_email}}
- Jackson Pollock - Dec 29, 2021 10:23 am + {{email.from_email}} + {{email.created}} Call sent - - -
- -
- - How to take the headache out of Order -
ansolo45@mail.com
- - Ansolo Lazinatov - Dec 27, 2021 3:27 pm - Call - delivered - - - -
- -
- - The Arnold Schwarzenegger of Order -
ansolo45@mail.com
- - Ansolo Lazinatov - Dec 24, 2021 10:44 am - Call - Bounce - - - -
- -
- - My order is not being taken -
jackson@mail.com
- - Jackson Pollock - Dec 19, 2021 4:55 pm - Call - Spam - - - -
- -
- - Shipment is missing -
jackson@mail.com
- - Jackson Pollock - Dec 19, 2021 2:43 pm - Call - sent - - - -
- -
- - How can I order something urgently? -
ansolo45@mail.com
- - Jackson Pollock - Dec 19, 2021 2:43 pm - Call - Delivered - - - -
- -
- - How the delicacy of the products will be handled? -
ansolo45@mail.com
- - Ansolo Lazinatov - Dec 16, 2021 5:18 pm - Call - bounced - + {% endfor %}
@@ -393,166 +311,22 @@ + {% for email in emails.draft %}
- Quary about purchased soccer socks -
jackson@mail.com
+ {{email.subject}} +
{{email.to_email}}
- Jackson Pollock - Dec 29, 2021 10:23 am - Call - sent - - - -
- -
- - How to take the headache out of Order -
ansolo45@mail.com
- - Ansolo Lazinatov - Dec 27, 2021 3:27 pm - Call - delivered - - - -
- -
- - The Arnold Schwarzenegger of Order -
ansolo45@mail.com
- - Ansolo Lazinatov - Dec 24, 2021 10:44 am - Call - Bounce - - - -
- -
- - My order is not being taken -
jackson@mail.com
- - Jackson Pollock - Dec 19, 2021 4:55 pm - Call - Spam - - - -
- -
- - Shipment is missing -
jackson@mail.com
- - Jackson Pollock - Dec 19, 2021 2:43 pm - Call - sent - - - -
-
- -
- -
    - -
    -
    -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {% endfor %}
    -
    - -
    -
    SubjectSent byDateActionStatus
    -
    - -
    -
    Quary about purchased soccer socks -
    jackson@mail.com
    -
    Jackson PollockDec 29, 2021 10:23 amCallsent
    -
    - -
    -
    How to take the headache out of Order -
    ansolo45@mail.com
    -
    Ansolo LazinatovDec 27, 2021 3:27 pmCalldelivered
    -
    - -
    -
    The Arnold Schwarzenegger of Order -
    ansolo45@mail.com
    -
    Ansolo LazinatovDec 24, 2021 10:44 amCallBounce
    -
    - -
    -
    My order is not being taken -
    jackson@mail.com
    -
    Jackson PollockDec 19, 2021 4:55 pmCallSpam{{email.from_email}}{{email.created}}Senddraft
    diff --git a/templates/crm/leads/lead_send.html b/templates/crm/leads/lead_send.html index a03ae45f..c083dcdc 100644 --- a/templates/crm/leads/lead_send.html +++ b/templates/crm/leads/lead_send.html @@ -12,18 +12,22 @@ {% csrf_token %}
    - +
    - +
    - + +
    +
    +
    - diff --git a/templates/crm/opportunities/opportunity_detail.html b/templates/crm/opportunities/opportunity_detail.html index 259037d7..710a1088 100644 --- a/templates/crm/opportunities/opportunity_detail.html +++ b/templates/crm/opportunities/opportunity_detail.html @@ -302,127 +302,52 @@
    + {% for activity in activities %}
    -
    +
    + {% if activity.activity_type == "call" %} + + {% elif activity.activity_type == "email" %} + + {% elif activity.activity_type == "visit" %} + + {% elif activity.activity_type == "whatsapp" %} + + {% endif %} +
    -
    Assigned as a director for Project The Chewing Gum Attack
    -

    byJackson Pollock

    +
    +

    by{{activity.created_by}}

    -
    22 September, 2022, 4:33 PM
    -
    -

    Utilizing best practices to better leverage our assets, we must engage in black sky leadership thinking, not the usual band-aid solution.

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    Onboarding Meeting
    -

    byJackson Pollock

    -
    -
    20 September, 2022, 5:31pm
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    Designing the dungeon
    -

    byJackson Pollock

    -
    -
    19 September, 2022, 4:39pm
    -
    -

    To get off the runway and paradigm shift, we should take brass tacks with above-the-board actionable analytics, ramp up with viral partnering, not the usual goat rodeo putting socks on an octopus.

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    Purchasing-Related Vendors
    -

    byAnsolo Lazinatov

    -
    -
    22 September, 2022, 4:30pm
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    Quary about purchased soccer socks
    -

    byAnsolo Lazinatov

    -
    -
    15 September, 2022, 3:33pm
    -
    -

    I’ve come across your posts and found some favorable deals on your page. I’ve added a load of products to the cart and I don’t know the payment options you avail. Also, can you enlighten me about any discount.

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    Added image
    -

    byAnsolo Lazinatov

    -
    -
    11 September, 2022, 12:15am
    +
    {{activity.created}}
    +

    + {% endfor %}

    Notes

    - -
    +
    + {% csrf_token %} + + +
    +
    + {% for note in notes %}
    -

    Gave us a nice feedback

    +

    {{ note.note }}

    -
    clock 12 Nov, 2018
    -

    byAnsolo Lazinatov

    +
    {{note.created}}
    +

    by{{note.created_by}}

    -
    -
    -

    I also want to let you know that I am available to you as your real estate insider from now on. If you have any questions about the market, even if they sound silly, call or text anytime.

    -
    -
    30 Jan, 2019
    -

    byAnsolo Lazinatov

    -
    -
    -
    -

    To get off the runway and paradigm shift, we should take brass tacks with above-the-board actionable analytics, ramp up with viral partnering, not the usual goat rodeo putting socks on an octopus.

    -
    -
    19 September, 2022, 4:39pm
    -

    byJackson Pollock

    -
    -
    -
    -

    Utilizing best practices to better leverage our assets, we must engage in black sky leadership thinking, not the usual band-aid solution.

    -
    -
    22 September, 2022, 4:30pm
    -

    byAnsolo Lazinatov

    -
    -
    +
    + {% endfor %}
    @@ -787,6 +712,7 @@ +
    @@ -796,7 +722,7 @@
    -
    Ansolo Lazinatov
    +
    Purchasing-Related Vendors Dec 29, 2021 @@ -813,7 +739,6 @@
    -
    diff --git a/templates/customers/view_customer.html b/templates/customers/view_customer.html index 86a449d7..0a59e721 100644 --- a/templates/customers/view_customer.html +++ b/templates/customers/view_customer.html @@ -73,18 +73,14 @@
    -
    +
    -
    {% trans 'Visits' %}
    -

    23

    -
    -
    -
    {% trans 'Calls' %}
    -

    9

    +
    {% trans 'Invoices' %}
    +

    {{invoices.count}}

    {% trans 'Quotations' %}
    -

    5

    +

    {{estimates.count}}

    @@ -98,11 +94,11 @@
    {{ _("Address") }}
    -

    {{ customer.address}}
    Riyadh,
    Saudi Arabia

    +

    {{ customer.address_1}}

    {% trans 'Email' %}
    {{ customer.email }}
    -
    {% trans 'Phone Number' %}
    {{ customer.phone_number }} +
    {% trans 'Phone Number' %}
    {{ customer.phone }}