diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 040e7fa..a5eaed3 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 21ff9b5..87e4473 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 1ac853d..d38b33f 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0008_quotation_sale_quotation_quotationitem.py b/core/migrations/0008_quotation_sale_quotation_quotationitem.py new file mode 100644 index 0000000..330807a --- /dev/null +++ b/core/migrations/0008_quotation_sale_quotation_quotationitem.py @@ -0,0 +1,45 @@ +# Generated by Django 5.2.7 on 2026-02-02 09:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Quotation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quotation_number', models.CharField(blank=True, max_length=50, verbose_name='Quotation Number')), + ('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')), + ('discount', models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Discount')), + ('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('converted', 'Converted to Invoice')], default='draft', max_length=20, verbose_name='Status')), + ('valid_until', models.DateField(blank=True, null=True, verbose_name='Valid Until')), + ('terms_and_conditions', models.TextField(blank=True, verbose_name='Terms and Conditions')), + ('notes', models.TextField(blank=True, verbose_name='Notes')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quotations', to='core.customer')), + ], + ), + migrations.AddField( + model_name='sale', + name='quotation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='converted_sale', to='core.quotation'), + ), + migrations.CreateModel( + name='QuotationItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(verbose_name='Quantity')), + ('unit_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price')), + ('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ('quotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.quotation')), + ], + ), + ] diff --git a/core/migrations/0009_purchasereturn_purchasereturnitem_salereturn_and_more.py b/core/migrations/0009_purchasereturn_purchasereturnitem_salereturn_and_more.py new file mode 100644 index 0000000..e215b8a --- /dev/null +++ b/core/migrations/0009_purchasereturn_purchasereturnitem_salereturn_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 5.2.7 on 2026-02-02 10:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_quotation_sale_quotation_quotationitem'), + ] + + operations = [ + migrations.CreateModel( + name='PurchaseReturn', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('return_number', models.CharField(blank=True, max_length=50, verbose_name='Return Number')), + ('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')), + ('notes', models.TextField(blank=True, verbose_name='Notes')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('purchase', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='returns', to='core.purchase')), + ('supplier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_returns', to='core.supplier')), + ], + ), + migrations.CreateModel( + name='PurchaseReturnItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(verbose_name='Quantity')), + ('cost_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Cost Price')), + ('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ('purchase_return', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.purchasereturn')), + ], + ), + migrations.CreateModel( + name='SaleReturn', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('return_number', models.CharField(blank=True, max_length=50, verbose_name='Return Number')), + ('total_amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount')), + ('notes', models.TextField(blank=True, verbose_name='Notes')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sale_returns', to='core.customer')), + ('sale', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='returns', to='core.sale')), + ], + ), + migrations.CreateModel( + name='SaleReturnItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(verbose_name='Quantity')), + ('unit_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price')), + ('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ('sale_return', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.salereturn')), + ], + ), + ] diff --git a/core/migrations/__pycache__/0008_quotation_sale_quotation_quotationitem.cpython-311.pyc b/core/migrations/__pycache__/0008_quotation_sale_quotation_quotationitem.cpython-311.pyc new file mode 100644 index 0000000..b628003 Binary files /dev/null and b/core/migrations/__pycache__/0008_quotation_sale_quotation_quotationitem.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0009_purchasereturn_purchasereturnitem_salereturn_and_more.cpython-311.pyc b/core/migrations/__pycache__/0009_purchasereturn_purchasereturnitem_salereturn_and_more.cpython-311.pyc new file mode 100644 index 0000000..40e202f Binary files /dev/null and b/core/migrations/__pycache__/0009_purchasereturn_purchasereturnitem_salereturn_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 4569aea..2c98bf5 100644 --- a/core/models.py +++ b/core/models.py @@ -71,6 +71,7 @@ class Sale(models.Model): ] customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales") + quotation = models.ForeignKey('Quotation', on_delete=models.SET_NULL, null=True, blank=True, related_name="converted_sale") invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True) total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0) @@ -117,6 +118,38 @@ class SalePayment(models.Model): def __str__(self): return f"Payment of {self.amount} for Sale #{self.sale.id}" +class Quotation(models.Model): + STATUS_CHOICES = [ + ('draft', _('Draft')), + ('sent', _('Sent')), + ('accepted', _('Accepted')), + ('rejected', _('Rejected')), + ('converted', _('Converted to Invoice')), + ] + + customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="quotations") + quotation_number = models.CharField(_("Quotation Number"), max_length=50, blank=True) + total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) + discount = models.DecimalField(_("Discount"), max_digits=15, decimal_places=3, default=0) + status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='draft') + valid_until = models.DateField(_("Valid Until"), null=True, blank=True) + terms_and_conditions = models.TextField(_("Terms and Conditions"), blank=True) + notes = models.TextField(_("Notes"), blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Quotation #{self.id} - {self.customer.name if self.customer else 'Guest'}" + +class QuotationItem(models.Model): + quotation = models.ForeignKey(Quotation, on_delete=models.CASCADE, related_name="items") + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(_("Quantity")) + unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) + line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) + + def __str__(self): + return f"{self.product.name_en} x {self.quantity}" + class Purchase(models.Model): PAYMENT_TYPE_CHOICES = [ ('cash', _('Cash')), @@ -175,6 +208,48 @@ class PurchasePayment(models.Model): def __str__(self): return f"Payment of {self.amount} for Purchase #{self.purchase.id}" +class SaleReturn(models.Model): + sale = models.ForeignKey(Sale, on_delete=models.SET_NULL, null=True, blank=True, related_name="returns") + customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_returns") + return_number = models.CharField(_("Return Number"), max_length=50, blank=True) + total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) + notes = models.TextField(_("Notes"), blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Sale Return #{self.id} - {self.customer.name if self.customer else 'Guest'}" + +class SaleReturnItem(models.Model): + sale_return = models.ForeignKey(SaleReturn, on_delete=models.CASCADE, related_name="items") + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(_("Quantity")) + unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) + line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) + + def __str__(self): + return f"{self.product.name_en} x {self.quantity}" + +class PurchaseReturn(models.Model): + purchase = models.ForeignKey(Purchase, on_delete=models.SET_NULL, null=True, blank=True, related_name="returns") + supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_returns") + return_number = models.CharField(_("Return Number"), max_length=50, blank=True) + total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) + notes = models.TextField(_("Notes"), blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Purchase Return #{self.id} - {self.supplier.name if self.supplier else 'N/A'}" + +class PurchaseReturnItem(models.Model): + purchase_return = models.ForeignKey(PurchaseReturn, on_delete=models.CASCADE, related_name="items") + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(_("Quantity")) + cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3) + line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3) + + def __str__(self): + return f"{self.product.name_en} x {self.quantity}" + class SystemSetting(models.Model): business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting") address = models.TextField(_("Address"), blank=True) @@ -187,4 +262,4 @@ class SystemSetting(models.Model): registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True) def __str__(self): - return self.business_name + return self.business_name \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 0602d98..9b79024 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -47,19 +47,31 @@ {% trans "Dashboard" %} + +
  • {% trans "Sales" %}
  • {% trans "POS System" %}
  • - - {% trans "Invoices" %} + + {% trans "New Sales" %}
  • - - {% trans "Reports" %} + + {% trans "Sales Invoices" %} + +
  • +
  • + + {% trans "Quotation" %} + +
  • +
  • + + {% trans "Sales Return" %}
  • @@ -70,10 +82,25 @@
  • - + {% trans "Purchases" %}
  • +
  • + +
  • + + {% trans "Barcode Printing" %} + +
  • + {% trans "Purchase Return" %} + + +
  • + + {% trans "Reports" %} + +
  • {% trans "Contacts" %}
  • @@ -93,6 +120,11 @@ {% trans "Settings" %}
  • +
  • + + {% trans "Django Admin" %} + +
  • diff --git a/core/templates/core/barcode_labels.html b/core/templates/core/barcode_labels.html new file mode 100644 index 0000000..72c7dcc --- /dev/null +++ b/core/templates/core/barcode_labels.html @@ -0,0 +1,429 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
    +
    +

    Barcode Label Printing

    + +
    + +
    + +
    +
    +
    +
    1. Select Products
    +
    +
    +
    + + +
    +
    + + + + + + + + + + {% for product in products %} + + + + + + {% endfor %} + +
    ProductSKUAction
    +
    {{ product.name_en }}
    + {{ product.name_ar }} +
    {{ product.sku }} + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    2. Label Queue & Settings
    +
    +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + +
    + + + + + + + + + + + +
    ProductQty of Labels
    +
    + + Queue is empty. Select products to start. +
    +
    +
    +
    + + +
    +
    +
    3. Live Preview (Single Label)
    +
    +
    +
    +
    +
    Product Name
    + + +
    +
    +
    + Note: This is a preview of the layout. Actual print layout depends on settings above. +
    +
    +
    +
    +
    +
    + + + + + + + + +{% endblock %} diff --git a/core/templates/core/invoice_detail.html b/core/templates/core/invoice_detail.html index a169ab7..33fed51 100644 --- a/core/templates/core/invoice_detail.html +++ b/core/templates/core/invoice_detail.html @@ -8,15 +8,20 @@
    - {% trans "Back to Invoices" %} + {% trans "Back to Invoices" %} / العودة إلى الفواتير - +
    + + +
    -
    +
    @@ -32,23 +37,23 @@

    {{ settings.phone }}

    {{ settings.email }}

    {% if settings.vat_number %} -

    {% trans "VAT" %}: {{ settings.vat_number }}

    +

    {% trans "VAT" %} / الضريبة: {{ settings.vat_number }}

    {% endif %}
    -
    -

    {% trans "Tax Invoice" %}

    -
    -
    {% trans "Invoice Number" %}
    +
    +

    {% trans "Tax Invoice" %} / فاتورة ضريبية

    +
    +
    {% trans "Invoice Number" %} / رقم الفاتورة
    {{ sale.invoice_number|default:sale.id }}
    -
    +
    -
    {% trans "Issue Date" %}
    +
    {% trans "Issue Date" %} / تاريخ الإصدار
    {{ sale.created_at|date:"Y-m-d" }}
    -
    {% trans "Due Date" %}
    +
    {% trans "Due Date" %} / تاريخ الاستحقاق
    {{ sale.due_date|date:"Y-m-d"|default:"-" }}
    @@ -57,7 +62,7 @@
    -
    {% trans "Customer Information" %}
    +
    {% trans "Customer Information" %} / معلومات العميل
    {{ sale.customer.name|default:_("Guest Customer") }}
    {% if sale.customer.phone %}
    {{ sale.customer.phone }}
    @@ -67,14 +72,14 @@ {% endif %}
    -
    {% trans "Payment Status" %}
    +
    {% trans "Payment Status" %} / حالة الدفع
    {% if sale.status == 'paid' %} - {% trans "Fully Paid" %} + {% trans "Fully Paid" %} / مدفوع بالكامل {% elif sale.status == 'partial' %} - {% trans "Partially Paid" %} + {% trans "Partially Paid" %} / مدفوع جزئياً {% else %} - {% trans "Unpaid" %} + {% trans "Unpaid" %} / غير مدفوع {% endif %}
    @@ -85,10 +90,22 @@ - - - - + + + + @@ -107,29 +124,44 @@ - + {% if sale.discount > 0 %} - + {% endif %} - + - + - + @@ -139,15 +171,15 @@ {% if sale.payments.exists %}
    -
    {% trans "Payment Records" %}
    +
    {% trans "Payment Records" %} / سجلات الدفع
    {% trans "Item Description" %}{% trans "Unit Price" %}{% trans "Quantity" %}{% trans "Total" %} +
    {% trans "Item Description" %}
    +
    وصف العنصر
    +
    +
    {% trans "Unit Price" %}
    +
    سعر الوحدة
    +
    +
    {% trans "Quantity" %}
    +
    الكمية
    +
    +
    {% trans "Total" %}
    +
    المجموع
    +
    {% trans "Subtotal" %} +
    {% trans "Subtotal" %}
    +
    المجموع الفرعي
    +
    {{ settings.currency_symbol }}{{ sale.total_amount|add:sale.discount|floatformat:3 }}
    {% trans "Discount" %} +
    {% trans "Discount" %}
    +
    الخصم
    +
    -{{ settings.currency_symbol }}{{ sale.discount|floatformat:3 }}
    {% trans "Grand Total" %} +
    {% trans "Grand Total" %}
    +
    المجموع الكلي
    +
    {{ settings.currency_symbol }}{{ sale.total_amount|floatformat:3 }}
    {% trans "Total Paid" %} +
    {% trans "Total Paid" %}
    +
    إجمالي المدفوع
    +
    {{ settings.currency_symbol }}{{ sale.paid_amount|floatformat:3 }}
    {% trans "Balance Due" %} +
    {% trans "Balance Due" %}
    +
    الرصيد المستحق
    +
    {{ settings.currency_symbol }}{{ sale.balance_due|floatformat:3 }}
    - - - - + + + + @@ -168,28 +200,65 @@ {% if sale.notes %}
    -
    {% trans "Internal Notes" %}
    +
    {% trans "Internal Notes" %} / ملاحظات داخلية

    {{ sale.notes }}

    {% endif %}
    -

    {% trans "Thank you for your business!" %}

    -

    {% trans "Software by Meezan" %}

    +

    {% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!

    +

    {% trans "Software by Meezan" %} / برمجة ميزان

    + + + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/purchase_detail.html b/core/templates/core/purchase_detail.html index d042075..d515b0e 100644 --- a/core/templates/core/purchase_detail.html +++ b/core/templates/core/purchase_detail.html @@ -8,15 +8,20 @@
    - {% trans "Back to List" %} + {% trans "Back to List" %} / العودة للقائمة - +
    + + +
    -
    +
    @@ -32,23 +37,23 @@

    {{ settings.phone }}

    {{ settings.email }}

    {% if settings.vat_number %} -

    {% trans "VAT" %}: {{ settings.vat_number }}

    +

    {% trans "VAT" %} / الضريبة: {{ settings.vat_number }}

    {% endif %}
    -

    {% trans "Purchase Invoice" %}

    +

    {% trans "Purchase Invoice" %} / فاتورة مشتريات

    -
    {% trans "Invoice Number" %}
    +
    {% trans "Invoice Number" %} / رقم الفاتورة
    {{ purchase.invoice_number|default:purchase.id }}
    -
    {% trans "Issue Date" %}
    +
    {% trans "Issue Date" %} / تاريخ الإصدار
    {{ purchase.created_at|date:"Y-m-d" }}
    -
    {% trans "Due Date" %}
    +
    {% trans "Due Date" %} / تاريخ الاستحقاق
    {{ purchase.due_date|date:"Y-m-d"|default:"-" }}
    @@ -57,7 +62,7 @@
    -
    {% trans "Supplier Information" %}
    +
    {% trans "Supplier Information" %} / معلومات المورد
    {{ purchase.supplier.name }}
    {% if purchase.supplier.phone %}
    {{ purchase.supplier.phone }}
    @@ -67,14 +72,14 @@ {% endif %}
    -
    {% trans "Payment Status" %}
    +
    {% trans "Payment Status" %} / حالة الدفع
    {% if purchase.status == 'paid' %} - {% trans "Fully Paid" %} + {% trans "Fully Paid" %} / مدفوع بالكامل {% elif purchase.status == 'partial' %} - {% trans "Partially Paid" %} + {% trans "Partially Paid" %} / مدفوع جزئياً {% else %} - {% trans "Unpaid" %} + {% trans "Unpaid" %} / غير مدفوع {% endif %}
    @@ -85,10 +90,22 @@
    {% trans "Date" %}{% trans "Method" %}{% trans "Amount" %}{% trans "Notes" %}{% trans "Date" %} / التاريخ{% trans "Method" %} / الطريقة{% trans "Amount" %} / المبلغ{% trans "Notes" %} / ملاحظات
    - - - - + + + + @@ -107,17 +124,26 @@ - + - + - + @@ -127,15 +153,15 @@ {% if purchase.payments.exists %}
    -
    {% trans "Payment History" %}
    +
    {% trans "Payment History" %} / سجل الدفعات
    {% trans "Item Description" %}{% trans "Cost Price" %}{% trans "Quantity" %}{% trans "Total" %} +
    {% trans "Item Description" %}
    +
    وصف العنصر
    +
    +
    {% trans "Cost Price" %}
    +
    سعر التكلفة
    +
    +
    {% trans "Quantity" %}
    +
    الكمية
    +
    +
    {% trans "Total" %}
    +
    المجموع
    +
    {% trans "Grand Total" %} +
    {% trans "Grand Total" %}
    +
    المجموع الكلي
    +
    {{ settings.currency_symbol }}{{ purchase.total_amount|floatformat:3 }}
    {% trans "Total Paid" %} +
    {% trans "Total Paid" %}
    +
    إجمالي المدفوع
    +
    {{ settings.currency_symbol }}{{ purchase.paid_amount|floatformat:3 }}
    {% trans "Balance Due" %} +
    {% trans "Balance Due" %}
    +
    الرصيد المستحق
    +
    {{ settings.currency_symbol }}{{ purchase.balance_due|floatformat:3 }}
    - - - - + + + + @@ -156,27 +182,64 @@ {% if purchase.notes %}
    -
    {% trans "Notes" %}
    +
    {% trans "Notes" %} / ملاحظات

    {{ purchase.notes }}

    {% endif %}
    -

    {% trans "Thank you for your business!" %}

    +

    {% trans "Thank you for your business!" %} / شكراً لتعاملكم معنا!

    + + + {% endblock %} \ No newline at end of file diff --git a/core/templates/core/purchase_return_create.html b/core/templates/core/purchase_return_create.html new file mode 100644 index 0000000..2dd5559 --- /dev/null +++ b/core/templates/core/purchase_return_create.html @@ -0,0 +1,255 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "New Purchase Return" %} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    +
    + +
    +
    +
    +
    {% trans "Create Purchase Return" %}
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + +
    +
    + +
    +
    +
    + + +
    +
    {% trans "Date" %}{% trans "Method" %}{% trans "Amount" %}{% trans "Notes" %}{% trans "Date" %} / التاريخ{% trans "Method" %} / الطريقة{% trans "Amount" %} / المبلغ{% trans "Notes" %} / ملاحظات
    + + + + + + + + + + + + + + + + + + + + + +
    {% trans "Product" %}{% trans "Cost Price" %}{% trans "Quantity" %}{% trans "Total" %}
    +
    [[ item.name_en ]]
    +
    [[ item.sku ]]
    +
    + + + + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + +
    + {% trans "Search and add products to this return." %} +
    +
    +
    +
    +
    + + +
    +
    +
    +
    {% trans "Return Summary" %}
    + +
    + {% trans "Total Amount" %} +

    [[ currencySymbol ]][[ subtotal.toFixed(3) ]]

    +
    + +
    + +
    + + +
    + +
    + + {% trans "Completing this return will automatically decrease the stock quantity for the selected items." %} +
    + +
    + +
    +
    +
    +
    +
    +
    + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/purchase_return_detail.html b/core/templates/core/purchase_return_detail.html new file mode 100644 index 0000000..6eb9c6a --- /dev/null +++ b/core/templates/core/purchase_return_detail.html @@ -0,0 +1,183 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Purchase Return" %} #{{ purchase_return.return_number|default:purchase_return.id }} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    + +
    + + {% trans "Back to Returns" %} / العودة إلى المرتجعات + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + {% if settings.logo %} + Logo + {% else %} +

    {{ settings.business_name }}

    + {% endif %} +
    +

    {{ settings.address }}

    +

    {{ settings.phone }}

    +

    {{ settings.email }}

    +
    +
    +
    +

    {% trans "Purchase Return" %} / مرتجع مشتريات

    +
    +
    {% trans "Return Number" %} / رقم المرتجع
    +
    {{ purchase_return.return_number|default:purchase_return.id }}
    +
    +
    +
    +
    {% trans "Return Date" %} / تاريخ المرتجع
    +
    {{ purchase_return.created_at|date:"Y-m-d" }}
    +
    +
    +
    {% trans "Original Purchase" %} / الشراء الأصلي
    +
    {% if purchase_return.purchase %}#{{ purchase_return.purchase.invoice_number|default:purchase_return.purchase.id }}{% else %}-{% endif %}
    +
    +
    +
    +
    + +
    +
    +
    {% trans "Supplier Information" %} / معلومات المورد
    +
    {{ purchase_return.supplier.name|default:"N/A" }}
    + {% if purchase_return.supplier.phone %} +
    {{ purchase_return.supplier.phone }}
    + {% endif %} +
    +
    + + +
    + + + + + + + + + + + {% for item in purchase_return.items.all %} + + + + + + + {% endfor %} + + + + + + + + +
    +
    {% trans "Item Description" %}
    +
    وصف العنصر
    +
    +
    {% trans "Cost Price" %}
    +
    سعر التكلفة
    +
    +
    {% trans "Quantity" %}
    +
    الكمية
    +
    +
    {% trans "Total" %}
    +
    المجموع
    +
    +
    {{ item.product.name_en }}
    +
    {{ item.product.name_ar }}
    +
    {{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }}{{ item.quantity }}{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}
    +
    {% trans "Total Credit" %}
    +
    إجمالي الرصيد المسترد
    +
    {{ settings.currency_symbol }}{{ purchase_return.total_amount|floatformat:3 }}
    +
    + + + {% if purchase_return.notes %} +
    +
    {% trans "Notes" %} / ملاحظات
    +

    {{ purchase_return.notes }}

    +
    + {% endif %} + +
    +

    {% trans "Purchase Return Confirmation" %} / تأكيد مرتجع مشتريات

    +

    {% trans "Software by Meezan" %} / برمجة ميزان

    +
    +
    +
    +
    +
    + + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/purchase_returns.html b/core/templates/core/purchase_returns.html new file mode 100644 index 0000000..5376828 --- /dev/null +++ b/core/templates/core/purchase_returns.html @@ -0,0 +1,105 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Purchase Returns" %} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    +
    +
    +

    {% trans "Purchase Returns" %}

    +

    {% trans "Manage returns to suppliers" %}

    +
    + + {% trans "New Purchase Return" %} + +
    + + {% if messages %} +
    + {% for message in messages %} + + {% endfor %} +
    + {% endif %} + +
    +
    +
    + + + + + + + + + + + + + {% for return in returns %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans "Return #" %}{% trans "Date" %}{% trans "Supplier" %}{% trans "Original Purchase" %}{% trans "Total Amount" %}{% trans "Actions" %}
    + {{ return.return_number|default:return.id }} + {{ return.created_at|date:"Y-m-d" }}{{ return.supplier.name|default:"N/A" }} + {% if return.purchase %} + + #{{ return.purchase.invoice_number|default:return.purchase.id }} + + {% else %} + N/A + {% endif %} + {{ site_settings.currency_symbol }}{{ return.total_amount|floatformat:3 }} +
    + + + + +
    + + + +
    + Empty +

    {% trans "No purchase returns found." %}

    +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/quotation_create.html b/core/templates/core/quotation_create.html new file mode 100644 index 0000000..2635ca0 --- /dev/null +++ b/core/templates/core/quotation_create.html @@ -0,0 +1,272 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "New Quotation" %} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    +
    + +
    +
    +
    +
    {% trans "Create New Quotation" %}
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + +
    +
    + +
    +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + +
    {% trans "Product" %}{% trans "Unit Price" %}{% trans "Quantity" %}{% trans "Total" %}
    +
    [[ item.name_en ]]
    +
    [[ item.sku ]]
    +
    + + + + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + +
    + {% trans "Search and add products to this quotation." %} +
    +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    {% trans "Quotation Summary" %}
    + +
    + {% trans "Subtotal" %} + [[ currencySymbol ]][[ subtotal.toFixed(3) ]] +
    + +
    + {% trans "Discount" %} +
    + +
    +
    + +
    + +
    +

    {% trans "Grand Total" %}

    +

    [[ currencySymbol ]][[ grandTotal.toFixed(3) ]]

    +
    + +
    + + +
    + +
    + + +
    + +
    + +
    +
    +
    +
    +
    +
    + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/quotation_detail.html b/core/templates/core/quotation_detail.html new file mode 100644 index 0000000..9cc9ca9 --- /dev/null +++ b/core/templates/core/quotation_detail.html @@ -0,0 +1,253 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Quotation" %} #{{ quotation.quotation_number|default:quotation.id }} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    + +
    + + {% trans "Back to Quotations" %} / العودة لعروض الأسعار + +
    + {% if quotation.status != 'converted' %} + + {% endif %} + + +
    +
    + + + + + +
    +
    + +
    +
    +
    + {% if settings.logo %} + Logo + {% else %} +

    {{ settings.business_name }}

    + {% endif %} +
    +

    {{ settings.address }}

    +

    {{ settings.phone }}

    +

    {{ settings.email }}

    + {% if settings.vat_number %} +

    {% trans "VAT" %} / الضريبة: {{ settings.vat_number }}

    + {% endif %} +
    +
    +
    +

    {% trans "Quotation" %} / عرض سعر

    +
    +
    {% trans "Quotation Number" %} / رقم العرض
    +
    {{ quotation.quotation_number|default:quotation.id }}
    +
    +
    +
    +
    {% trans "Date" %} / التاريخ
    +
    {{ quotation.created_at|date:"Y-m-d" }}
    +
    +
    +
    {% trans "Valid Until" %} / صالح لغاية
    +
    {{ quotation.valid_until|date:"Y-m-d"|default:"-" }}
    +
    +
    +
    +
    + +
    +
    +
    {% trans "Quote For" %} / عرض مقدم إلى
    +
    {{ quotation.customer.name|default:_("Guest Customer") }}
    + {% if quotation.customer.phone %} +
    {{ quotation.customer.phone }}
    + {% endif %} + {% if quotation.customer.address %} +
    {{ quotation.customer.address }}
    + {% endif %} +
    +
    +
    {% trans "Status" %} / الحالة
    +
    + {% if quotation.status == 'converted' %} + {% trans "Converted" %} / محول + {% elif quotation.status == 'accepted' %} + {% trans "Accepted" %} / مقبول + {% elif quotation.status == 'rejected' %} + {% trans "Rejected" %} / مرفوض + {% else %} + {% trans "Open" %} / مفتوح + {% endif %} +
    +
    +
    + + +
    + + + + + + + + + + + {% for item in quotation.items.all %} + + + + + + + {% endfor %} + + + + + + + + {% if quotation.discount > 0 %} + + + + + + {% endif %} + + + + + + +
    +
    {% trans "Item Description" %}
    +
    وصف العنصر
    +
    +
    {% trans "Unit Price" %}
    +
    سعر الوحدة
    +
    +
    {% trans "Quantity" %}
    +
    الكمية
    +
    +
    {% trans "Total" %}
    +
    المجموع
    +
    +
    {{ item.product.name_en }}
    +
    {{ item.product.name_ar }}
    +
    {{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}{{ item.quantity }}{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}
    +
    {% trans "Subtotal" %}
    +
    المجموع الفرعي
    +
    {{ settings.currency_symbol }}{{ quotation.total_amount|add:quotation.discount|floatformat:3 }}
    +
    {% trans "Discount" %}
    +
    الخصم
    +
    -{{ settings.currency_symbol }}{{ quotation.discount|floatformat:3 }}
    +
    {% trans "Grand Total" %}
    +
    المجموع الكلي
    +
    {{ settings.currency_symbol }}{{ quotation.total_amount|floatformat:3 }}
    +
    + + +
    +
    {% trans "Terms and Conditions" %} / الشروط والأحكام
    +
    + {{ quotation.terms_and_conditions|default:_("No specific terms provided.") }} +
    +
    + + + {% if quotation.notes %} +
    +
    {% trans "Internal Notes" %} / ملاحظات داخلية
    +

    {{ quotation.notes }}

    +
    + {% endif %} + +
    +

    {% trans "This is a computer generated quotation." %} / هذا عرض سعر تم إنشاؤه بواسطة الكمبيوتر.

    +

    {% trans "Software by Meezan" %} / برمجة ميزان

    +
    +
    +
    +
    +
    + + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/quotations.html b/core/templates/core/quotations.html new file mode 100644 index 0000000..ce04f89 --- /dev/null +++ b/core/templates/core/quotations.html @@ -0,0 +1,135 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Quotations" %} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    +
    +
    +

    {% trans "Quotations" %}

    +

    {% trans "Manage and track your price proposals" %}

    +
    + + {% trans "New Quotation" %} + +
    + + {% if messages %} +
    + {% for message in messages %} + + {% endfor %} +
    + {% endif %} + +
    +
    +
    + + + + + + + + + + + + + + {% for q in quotations %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans "Quotation #" %}{% trans "Date" %}{% trans "Customer" %}{% trans "Total" %}{% trans "Status" %}{% trans "Valid Until" %}{% trans "Actions" %}
    + {{ q.quotation_number|default:q.id }} + {{ q.created_at|date:"Y-m-d" }}{{ q.customer.name|default:_("Guest") }}{{ site_settings.currency_symbol }}{{ q.total_amount|floatformat:3 }} + {% if q.status == 'draft' %} + {% trans "Draft" %} + {% elif q.status == 'sent' %} + {% trans "Sent" %} + {% elif q.status == 'accepted' %} + {% trans "Accepted" %} + {% elif q.status == 'converted' %} + {% trans "Converted" %} + {% elif q.status == 'rejected' %} + {% trans "Rejected" %} + {% endif %} + {{ q.valid_until|date:"Y-m-d"|default:"-" }} +
    + + + + {% if q.status != 'converted' %} + + {% endif %} + +
    + + + + + + +
    + Empty +

    {% trans "No quotations found." %}

    +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/sale_return_create.html b/core/templates/core/sale_return_create.html new file mode 100644 index 0000000..132a434 --- /dev/null +++ b/core/templates/core/sale_return_create.html @@ -0,0 +1,255 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "New Sales Return" %} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    +
    + +
    +
    +
    +
    {% trans "Create Sales Return" %}
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + +
    +
    + +
    +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + +
    {% trans "Product" %}{% trans "Unit Price" %}{% trans "Quantity" %}{% trans "Total" %}
    +
    [[ item.name_en ]]
    +
    [[ item.sku ]]
    +
    + + + + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(3) ]] + +
    + {% trans "Search and add products to this return." %} +
    +
    +
    +
    +
    + + +
    +
    +
    +
    {% trans "Return Summary" %}
    + +
    + {% trans "Total Amount" %} +

    [[ currencySymbol ]][[ subtotal.toFixed(3) ]]

    +
    + +
    + +
    + + +
    + +
    + + {% trans "Completing this return will automatically increase the stock quantity for the selected items." %} +
    + +
    + +
    +
    +
    +
    +
    +
    + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/sale_return_detail.html b/core/templates/core/sale_return_detail.html new file mode 100644 index 0000000..fb61b03 --- /dev/null +++ b/core/templates/core/sale_return_detail.html @@ -0,0 +1,183 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Sales Return" %} #{{ sale_return.return_number|default:sale_return.id }} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    + +
    + + {% trans "Back to Returns" %} / العودة إلى المرتجعات + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + {% if settings.logo %} + Logo + {% else %} +

    {{ settings.business_name }}

    + {% endif %} +
    +

    {{ settings.address }}

    +

    {{ settings.phone }}

    +

    {{ settings.email }}

    +
    +
    +
    +

    {% trans "Sales Return" %} / مرتجع مبيعات

    +
    +
    {% trans "Return Number" %} / رقم المرتجع
    +
    {{ sale_return.return_number|default:sale_return.id }}
    +
    +
    +
    +
    {% trans "Return Date" %} / تاريخ المرتجع
    +
    {{ sale_return.created_at|date:"Y-m-d" }}
    +
    +
    +
    {% trans "Original Sale" %} / البيع الأصلي
    +
    {% if sale_return.sale %}#{{ sale_return.sale.invoice_number|default:sale_return.sale.id }}{% else %}-{% endif %}
    +
    +
    +
    +
    + +
    +
    +
    {% trans "Customer Information" %} / معلومات العميل
    +
    {{ sale_return.customer.name|default:_("Guest Customer") }}
    + {% if sale_return.customer.phone %} +
    {{ sale_return.customer.phone }}
    + {% endif %} +
    +
    + + +
    + + + + + + + + + + + {% for item in sale_return.items.all %} + + + + + + + {% endfor %} + + + + + + + + +
    +
    {% trans "Item Description" %}
    +
    وصف العنصر
    +
    +
    {% trans "Unit Price" %}
    +
    سعر الوحدة
    +
    +
    {% trans "Quantity" %}
    +
    الكمية
    +
    +
    {% trans "Total" %}
    +
    المجموع
    +
    +
    {{ item.product.name_en }}
    +
    {{ item.product.name_ar }}
    +
    {{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }}{{ item.quantity }}{{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }}
    +
    {% trans "Total Refund" %}
    +
    إجمالي المبلغ المرتجع
    +
    {{ settings.currency_symbol }}{{ sale_return.total_amount|floatformat:3 }}
    +
    + + + {% if sale_return.notes %} +
    +
    {% trans "Notes" %} / ملاحظات
    +

    {{ sale_return.notes }}

    +
    + {% endif %} + +
    +

    {% trans "Sales Return Confirmation" %} / تأكيد مرتجع مبيعات

    +

    {% trans "Software by Meezan" %} / برمجة ميزان

    +
    +
    +
    +
    +
    + + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/sales_returns.html b/core/templates/core/sales_returns.html new file mode 100644 index 0000000..838249d --- /dev/null +++ b/core/templates/core/sales_returns.html @@ -0,0 +1,105 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Sales Returns" %} | {{ site_settings.business_name }}{% endblock %} + +{% block content %} +
    +
    +
    +

    {% trans "Sales Returns" %}

    +

    {% trans "Manage customer product returns" %}

    +
    + + {% trans "New Sales Return" %} + +
    + + {% if messages %} +
    + {% for message in messages %} + + {% endfor %} +
    + {% endif %} + +
    +
    +
    + + + + + + + + + + + + + {% for return in returns %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans "Return #" %}{% trans "Date" %}{% trans "Customer" %}{% trans "Original Sale" %}{% trans "Total Amount" %}{% trans "Actions" %}
    + {{ return.return_number|default:return.id }} + {{ return.created_at|date:"Y-m-d" }}{{ return.customer.name|default:_("Guest") }} + {% if return.sale %} + + #{{ return.sale.invoice_number|default:return.sale.id }} + + {% else %} + N/A + {% endif %} + {{ site_settings.currency_symbol }}{{ return.total_amount|floatformat:3 }} +
    + + + + +
    + + + +
    + Empty +

    {% trans "No sales returns found." %}

    +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index d0213eb..a8f3132 100644 --- a/core/urls.py +++ b/core/urls.py @@ -12,18 +12,40 @@ urlpatterns = [ path('settings/', views.settings_view, name='settings'), # Invoices (Sales) - path('invoices/', views.invoice_list, name='invoices'), # Changed to 'invoices' for consistency with sidebar + path('invoices/', views.invoice_list, name='invoices'), path('invoices/create/', views.invoice_create, name='invoice_create'), path('invoices//', views.invoice_detail, name='invoice_detail'), path('invoices/payment//', views.add_sale_payment, name='add_sale_payment'), path('invoices/delete//', views.delete_sale, name='delete_sale'), + # Quotations + path('quotations/', views.quotations, name='quotations'), + path('quotations/create/', views.quotation_create, name='quotation_create'), + path('quotations//', views.quotation_detail, name='quotation_detail'), + path('quotations/convert//', views.convert_quotation_to_invoice, name='convert_quotation_to_invoice'), + path('quotations/delete//', views.delete_quotation, name='delete_quotation'), + path('api/create-quotation/', views.create_quotation_api, name='create_quotation_api'), + + # Sales Returns + path('sales/returns/', views.sales_returns, name='sales_returns'), + path('sales/returns/create/', views.sale_return_create, name='sale_return_create'), + path('sales/returns//', views.sale_return_detail, name='sale_return_detail'), + path('sales/returns/delete//', views.delete_sale_return, name='delete_sale_return'), + path('api/create-sale-return/', views.create_sale_return_api, name='create_sale_return_api'), + # Purchases (Invoices) path('purchases/create/', views.purchase_create, name='purchase_create'), path('purchases//', views.purchase_detail, name='purchase_detail'), path('purchases/payment//', views.add_purchase_payment, name='add_purchase_payment'), path('purchases/delete//', views.delete_purchase, name='delete_purchase'), + # Purchase Returns + path('purchases/returns/', views.purchase_returns, name='purchase_returns'), + path('purchases/returns/create/', views.purchase_return_create, name='purchase_return_create'), + path('purchases/returns//', views.purchase_return_detail, name='purchase_return_detail'), + path('purchases/returns/delete//', views.delete_purchase_return, name='delete_purchase_return'), + path('api/create-purchase-return/', views.create_purchase_return_api, name='create_purchase_return_api'), + # API / Actions path('api/create-sale/', views.create_sale_api, name='create_sale_api'), path('api/create-purchase/', views.create_purchase_api, name='create_purchase_api'), @@ -42,6 +64,7 @@ urlpatterns = [ path('inventory/add/', views.add_product, name='add_product'), path('inventory/edit//', views.edit_product, name='edit_product'), path('inventory/delete//', views.delete_product, name='delete_product'), + path('inventory/barcodes/', views.barcode_labels, name='barcode_labels'), # Categories path('inventory/category/add/', views.add_category, name='add_category'), @@ -52,4 +75,4 @@ urlpatterns = [ path('inventory/unit/add/', views.add_unit, name='add_unit'), path('inventory/unit/edit//', views.edit_unit, name='edit_unit'), path('inventory/unit/delete//', views.delete_unit, name='delete_unit'), -] \ No newline at end of file +] diff --git a/core/views.py b/core/views.py index bb5c1a2..ca90f7f 100644 --- a/core/views.py +++ b/core/views.py @@ -6,7 +6,9 @@ from django.views.decorators.csrf import csrf_exempt from .models import ( Product, Sale, Category, Unit, Customer, Supplier, Purchase, PurchaseItem, PurchasePayment, - SaleItem, SalePayment, SystemSetting + SaleItem, SalePayment, SystemSetting, + Quotation, QuotationItem, + SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem ) import json from datetime import timedelta @@ -348,6 +350,263 @@ def delete_sale(request, pk): messages.success(request, _("Sale deleted successfully!")) return redirect('invoices') +# --- Quotation Views --- + +def quotations(request): + quotations_list = Quotation.objects.all().order_by('-created_at') + customers = Customer.objects.all() + return render(request, 'core/quotations.html', {'quotations': quotations_list, 'customers': customers}) + +def quotation_create(request): + products = Product.objects.filter(is_active=True) + customers = Customer.objects.all() + return render(request, 'core/quotation_create.html', {'products': products, 'customers': customers}) + +def quotation_detail(request, pk): + quotation = get_object_or_404(Quotation, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/quotation_detail.html', {'quotation': quotation, 'settings': settings}) + +@csrf_exempt +def create_quotation_api(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + customer_id = data.get('customer_id') + quotation_number = data.get('quotation_number', '') + items = data.get('items', []) + total_amount = data.get('total_amount', 0) + discount = data.get('discount', 0) + valid_until = data.get('valid_until') + terms_and_conditions = data.get('terms_and_conditions', '') + notes = data.get('notes', '') + + customer = None + if customer_id: + customer = Customer.objects.get(id=customer_id) + + quotation = Quotation.objects.create( + customer=customer, + quotation_number=quotation_number, + total_amount=total_amount, + discount=discount, + valid_until=valid_until if valid_until else None, + terms_and_conditions=terms_and_conditions, + notes=notes + ) + + for item in items: + product = Product.objects.get(id=item['id']) + QuotationItem.objects.create( + quotation=quotation, + product=product, + quantity=item['quantity'], + unit_price=item['price'], + line_total=item['line_total'] + ) + + return JsonResponse({'success': True, 'quotation_id': quotation.id}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}, status=400) + return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) + +def convert_quotation_to_invoice(request, pk): + quotation = get_object_or_404(Quotation, pk=pk) + if quotation.status == 'converted': + messages.warning(request, _("This quotation has already been converted to an invoice.")) + return redirect('invoices') + + # Create Sale from Quotation + sale = Sale.objects.create( + customer=quotation.customer, + quotation=quotation, + total_amount=quotation.total_amount, + discount=quotation.discount, + balance_due=quotation.total_amount, + payment_type='cash', + status='unpaid', + notes=quotation.notes + ) + + # Create SaleItems and Update Stock + for item in quotation.items.all(): + SaleItem.objects.create( + sale=sale, + product=item.product, + quantity=item.quantity, + unit_price=item.unit_price, + line_total=item.line_total + ) + # Deduct Stock + item.product.stock_quantity -= item.quantity + item.product.save() + + # Update Quotation Status + quotation.status = 'converted' + quotation.save() + + messages.success(request, _("Quotation converted to Invoice successfully!")) + return redirect('invoice_detail', pk=sale.pk) + +def delete_quotation(request, pk): + quotation = get_object_or_404(Quotation, pk=pk) + quotation.delete() + messages.success(request, _("Quotation deleted successfully!")) + return redirect('quotations') + +# --- Sale Return Views --- + +def sales_returns(request): + returns = SaleReturn.objects.all().order_by('-created_at') + return render(request, 'core/sales_returns.html', {'returns': returns}) + +def sale_return_create(request): + products = Product.objects.filter(is_active=True) + customers = Customer.objects.all() + sales = Sale.objects.all().order_by('-created_at') + return render(request, 'core/sale_return_create.html', { + 'products': products, + 'customers': customers, + 'sales': sales + }) + +def sale_return_detail(request, pk): + sale_return = get_object_or_404(SaleReturn, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/sale_return_detail.html', {'sale_return': sale_return, 'settings': settings}) + +@csrf_exempt +def create_sale_return_api(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + sale_id = data.get('sale_id') + customer_id = data.get('customer_id') + return_number = data.get('return_number', '') + items = data.get('items', []) + total_amount = data.get('total_amount', 0) + notes = data.get('notes', '') + + customer = None + if customer_id: + customer = Customer.objects.get(id=customer_id) + + sale = None + if sale_id: + sale = Sale.objects.get(id=sale_id) + + sale_return = SaleReturn.objects.create( + sale=sale, + customer=customer, + return_number=return_number, + total_amount=total_amount, + notes=notes + ) + + for item in items: + product = Product.objects.get(id=item['id']) + SaleReturnItem.objects.create( + sale_return=sale_return, + product=product, + quantity=item['quantity'], + unit_price=item['price'], + line_total=item['line_total'] + ) + # Increase Stock for Sales Return + product.stock_quantity += int(item['quantity']) + product.save() + + return JsonResponse({'success': True, 'return_id': sale_return.id}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}, status=400) + return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) + +def delete_sale_return(request, pk): + sale_return = get_object_or_404(SaleReturn, pk=pk) + for item in sale_return.items.all(): + item.product.stock_quantity -= item.quantity + item.product.save() + sale_return.delete() + messages.success(request, _("Sale return deleted successfully!")) + return redirect('sales_returns') + + +# --- Purchase Return Views --- + +def purchase_returns(request): + returns = PurchaseReturn.objects.all().order_by('-created_at') + return render(request, 'core/purchase_returns.html', {'returns': returns}) + +def purchase_return_create(request): + products = Product.objects.filter(is_active=True) + suppliers = Supplier.objects.all() + purchases = Purchase.objects.all().order_by('-created_at') + return render(request, 'core/purchase_return_create.html', { + 'products': products, + 'suppliers': suppliers, + 'purchases': purchases + }) + +def purchase_return_detail(request, pk): + purchase_return = get_object_or_404(PurchaseReturn, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/purchase_return_detail.html', {'purchase_return': purchase_return, 'settings': settings}) + +@csrf_exempt +def create_purchase_return_api(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + purchase_id = data.get('purchase_id') + supplier_id = data.get('supplier_id') + return_number = data.get('return_number', '') + items = data.get('items', []) + total_amount = data.get('total_amount', 0) + notes = data.get('notes', '') + + supplier = None + if supplier_id: + supplier = Supplier.objects.get(id=supplier_id) + + purchase = None + if purchase_id: + purchase = Purchase.objects.get(id=purchase_id) + + purchase_return = PurchaseReturn.objects.create( + purchase=purchase, + supplier=supplier, + return_number=return_number, + total_amount=total_amount, + notes=notes + ) + + for item in items: + product = Product.objects.get(id=item['id']) + PurchaseReturnItem.objects.create( + purchase_return=purchase_return, + product=product, + quantity=item['quantity'], + cost_price=item['price'], + line_total=item['line_total'] + ) + # Decrease Stock for Purchase Return + product.stock_quantity -= int(item['quantity']) + product.save() + + return JsonResponse({'success': True, 'return_id': purchase_return.id}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}, status=400) + return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) + +def delete_purchase_return(request, pk): + purchase_return = get_object_or_404(PurchaseReturn, pk=pk) + for item in purchase_return.items.all(): + item.product.stock_quantity += item.quantity + item.product.save() + purchase_return.delete() + messages.success(request, _("Purchase return deleted successfully!")) + return redirect('purchase_returns') + # --- Other Management Views --- def reports(request): @@ -575,3 +834,8 @@ def delete_unit(request, pk): unit.delete() messages.success(request, "Unit deleted successfully!") return redirect('inventory') + +def barcode_labels(request): + products = Product.objects.filter(is_active=True).order_by('name_en') + context = {'products': products} + return render(request, 'core/barcode_labels.html', context)