diff --git a/core/__pycache__/forms_import.cpython-311.pyc b/core/__pycache__/forms_import.cpython-311.pyc new file mode 100644 index 0000000..200f5f2 Binary files /dev/null and b/core/__pycache__/forms_import.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 747a1b1..cab57bd 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 27570f7..1f239d2 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_import.cpython-311.pyc b/core/__pycache__/views_import.cpython-311.pyc new file mode 100644 index 0000000..e15ea0c Binary files /dev/null and b/core/__pycache__/views_import.cpython-311.pyc differ diff --git a/core/forms_import.py b/core/forms_import.py new file mode 100644 index 0000000..2492c6b --- /dev/null +++ b/core/forms_import.py @@ -0,0 +1,4 @@ +from django import forms + +class ImportFileForm(forms.Form): + file = forms.FileField(label="Excel File (.xlsx)") diff --git a/core/models.py b/core/models.py index ccc705e..be90b4d 100644 --- a/core/models.py +++ b/core/models.py @@ -195,6 +195,7 @@ class SalePayment(models.Model): payment_method_name = models.CharField(_("Payment Method Name"), max_length=50, default="Cash") # Fallback notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Payment of {self.amount} for Sale #{self.sale.id}" @@ -322,6 +323,7 @@ class PurchasePayment(models.Model): payment_method_name = models.CharField(_("Payment Method Name"), max_length=50, default="Cash") # Fallback notes = models.TextField(_("Notes"), blank=True) created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"Payment of {self.amount} for Purchase #{self.purchase.id}" diff --git a/core/templates/core/import_categories.html b/core/templates/core/import_categories.html new file mode 100644 index 0000000..a27c90d --- /dev/null +++ b/core/templates/core/import_categories.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{% trans "Import Categories" %}

+ +
+ +
+
+
+
+
{% trans "Upload Excel File" %}
+
+
+
+ {% trans "Excel Format Instructions:" %}
+
    +
  • {% trans "Column A" %}: {% trans "Name (English) - Required" %}
  • +
  • {% trans "Column B" %}: {% trans "Name (Arabic) - Optional (Defaults to English name if blank)" %}
  • +
+ {% trans "Please skip the first row (header)." %} +
+ +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/import_suppliers.html b/core/templates/core/import_suppliers.html new file mode 100644 index 0000000..553969c --- /dev/null +++ b/core/templates/core/import_suppliers.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{% trans "Import Suppliers" %}

+ +
+ +
+
+
+
+
{% trans "Upload Excel File" %}
+
+
+
+ {% trans "Excel Format Instructions:" %}
+
    +
  • {% trans "Column A" %}: {% trans "Supplier Name - Required" %}
  • +
  • {% trans "Column B" %}: {% trans "Contact Person - Optional" %}
  • +
  • {% trans "Column C" %}: {% trans "Phone - Optional" %}
  • +
+ {% trans "Please skip the first row (header)." %} +
+ +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/inventory.html b/core/templates/core/inventory.html index 39f1e0e..2411165 100644 --- a/core/templates/core/inventory.html +++ b/core/templates/core/inventory.html @@ -25,6 +25,9 @@ + + {% trans "Import Categories" %} + @@ -774,4 +777,4 @@ } }); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/suppliers.html b/core/templates/core/suppliers.html index 5fa1e18..2e2753c 100644 --- a/core/templates/core/suppliers.html +++ b/core/templates/core/suppliers.html @@ -15,9 +15,14 @@ - +
+ + + {% trans "Import" %} + +
{% if messages %} @@ -60,6 +65,39 @@ + + + {% endfor %} @@ -135,4 +173,4 @@ document.getElementById('saveAndAddAnotherSupp').onclick = () => saveSupplier(true); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/urls.py b/core/urls.py index f3ffbce..b341509 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,5 +1,6 @@ from django.urls import path from . import views +from . import views_import urlpatterns = [ path('', views.index, name='index'), @@ -92,6 +93,7 @@ urlpatterns = [ path('suppliers/edit//', views.edit_supplier, name='edit_supplier'), path('suppliers/delete//', views.delete_supplier, name='delete_supplier'), path('api/add-supplier-ajax/', views.add_supplier_ajax, name='add_supplier_ajax'), + path('suppliers/import/', views_import.import_suppliers, name='import_suppliers'), # Inventory path('inventory/suggest-sku/', views.suggest_sku, name='suggest_sku'), @@ -106,6 +108,7 @@ urlpatterns = [ path('inventory/category/edit//', views.edit_category, name='edit_category'), path('inventory/category/delete//', views.delete_category, name='delete_category'), path('api/add-category-ajax/', views.add_category_ajax, name='add_category_ajax'), + path('inventory/category/import/', views_import.import_categories, name='import_categories'), # Units path('inventory/unit/add/', views.add_unit, name='add_unit'), @@ -147,4 +150,4 @@ urlpatterns = [ path('sessions/start/', views.start_session, name='start_session'), path('sessions/close/', views.close_session, name='close_session'), path('sessions//', views.session_detail, name='session_detail'), -] \ No newline at end of file +] diff --git a/core/views_import.py b/core/views_import.py new file mode 100644 index 0000000..608ecf4 --- /dev/null +++ b/core/views_import.py @@ -0,0 +1,139 @@ +from django.shortcuts import render, redirect +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.urls import reverse +from django.utils.text import slugify +import openpyxl +from .models import Category, Supplier +from .forms_import import ImportFileForm + +@login_required +def import_categories(request): + """ + Import categories from an Excel (.xlsx) file. + Expected columns: Name (Eng), Name (Ar) + """ + if request.method == 'POST': + form = ImportFileForm(request.POST, request.FILES) + if form.is_valid(): + excel_file = request.FILES['file'] + + try: + wb = openpyxl.load_workbook(excel_file) + sheet = wb.active + + count = 0 + updated_count = 0 + errors = [] + + # Skip header row (min_row=2) + for i, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): + if not any(row): continue # Skip empty rows + + # Unpack columns with fallbacks for safety + # Format: name_en, name_ar + name_en = str(row[0]).strip() if row[0] else None + name_ar = str(row[1]).strip() if len(row) > 1 and row[1] else name_en + + if not name_en: + errors.append(f"Row {i}: Missing English Name. Skipped.") + continue + + slug = slugify(name_en) + + category, created = Category.objects.update_or_create( + slug=slug, + defaults={ + 'name_en': name_en, + 'name_ar': name_ar, + } + ) + + if created: + count += 1 + else: + updated_count += 1 + + if count > 0 or updated_count > 0: + msg = f"Import completed: {count} new categories added" + if updated_count > 0: + msg += f", {updated_count} categories updated" + messages.success(request, msg) + + if errors: + for error in errors: + messages.warning(request, error) + + except Exception as e: + messages.error(request, f"Error processing file: {str(e)}") + + return redirect(reverse('inventory') + '#categories-list') + else: + form = ImportFileForm() + + return render(request, 'core/import_categories.html', {'form': form}) + +@login_required +def import_suppliers(request): + """ + Import suppliers from an Excel (.xlsx) file. + Expected columns: Name, Contact Person, Phone + """ + if request.method == 'POST': + form = ImportFileForm(request.POST, request.FILES) + if form.is_valid(): + excel_file = request.FILES['file'] + + try: + wb = openpyxl.load_workbook(excel_file) + sheet = wb.active + + count = 0 + updated_count = 0 + errors = [] + + # Skip header row (min_row=2) + for i, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): + if not any(row): continue # Skip empty rows + + # Unpack columns with fallbacks for safety + # Format: Name, Contact Person, Phone + name = str(row[0]).strip() if row[0] else None + contact_person = str(row[1]).strip() if len(row) > 1 and row[1] else '' + phone = str(row[2]).strip() if len(row) > 2 and row[2] else '' + + if not name: + errors.append(f"Row {i}: Missing Name. Skipped.") + continue + + supplier, created = Supplier.objects.update_or_create( + name=name, + defaults={ + 'contact_person': contact_person, + 'phone': phone, + } + ) + + if created: + count += 1 + else: + updated_count += 1 + + if count > 0 or updated_count > 0: + msg = f"Import completed: {count} new suppliers added" + if updated_count > 0: + msg += f", {updated_count} suppliers updated" + messages.success(request, msg) + + if errors: + for error in errors: + messages.warning(request, error) + + except Exception as e: + messages.error(request, f"Error processing file: {str(e)}") + + return redirect('suppliers') + else: + form = ImportFileForm() + + return render(request, 'core/import_suppliers.html', {'form': form}) \ No newline at end of file diff --git a/patch_models_timestamp.py b/patch_models_timestamp.py new file mode 100644 index 0000000..73634d9 --- /dev/null +++ b/patch_models_timestamp.py @@ -0,0 +1,43 @@ +import os + +path = 'core/models.py' +with open(path, 'r') as f: + content = f.read() + +# Patch SalePayment +old_sale_payment = """ notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") + + def __str__(self):""" +new_sale_payment = """ notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self):""" + +# Patch PurchasePayment +old_purchase_payment = """ notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") + + def __str__(self):""" +new_purchase_payment = """ notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self):""" + +# Check if SalePayment already has created_at +# A simple check: if we find the new pattern, we skip +if new_sale_payment in content: + print("SalePayment already patched.") +else: + content = content.replace(old_sale_payment, new_sale_payment) + +if new_purchase_payment in content: + print("PurchasePayment already patched.") +else: + content = content.replace(old_purchase_payment, new_purchase_payment) + +with open(path, 'w') as f: + f.write(content) +print("Patched core/models.py")