adding imprt export categ, suppliers

This commit is contained in:
Flatlogic Bot 2026-02-06 12:25:31 +00:00
parent 40404a2947
commit 42e2393347
13 changed files with 347 additions and 6 deletions

Binary file not shown.

Binary file not shown.

4
core/forms_import.py Normal file
View File

@ -0,0 +1,4 @@
from django import forms
class ImportFileForm(forms.Form):
file = forms.FileField(label="Excel File (.xlsx)")

View File

@ -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}"

View File

@ -0,0 +1,54 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{% trans "Import Categories" %}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'inventory' %}#categories-list" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> {% trans "Back to Inventory" %}
</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">{% trans "Upload Excel File" %}</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> {% trans "Excel Format Instructions:" %}<br>
<ul>
<li><strong>{% trans "Column A" %}:</strong> {% trans "Name (English) - Required" %}</li>
<li><strong>{% trans "Column B" %}:</strong> {% trans "Name (Arabic) - Optional (Defaults to English name if blank)" %}</li>
</ul>
<small>{% trans "Please skip the first row (header)." %}</small>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-upload"></i> {% trans "Import Categories" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{% trans "Import Suppliers" %}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'suppliers' %}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> {% trans "Back to Suppliers" %}
</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">{% trans "Upload Excel File" %}</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> {% trans "Excel Format Instructions:" %}<br>
<ul>
<li><strong>{% trans "Column A" %}:</strong> {% trans "Supplier Name - Required" %}</li>
<li><strong>{% trans "Column B" %}:</strong> {% trans "Contact Person - Optional" %}</li>
<li><strong>{% trans "Column C" %}:</strong> {% trans "Phone - Optional" %}</li>
</ul>
<small>{% trans "Please skip the first row (header)." %}</small>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-upload"></i> {% trans "Import Suppliers" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -25,6 +25,9 @@
<button class="btn btn-outline-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
<i class="bi bi-folder-plus me-2"></i>{% trans "Add Category" %}
</button>
<a href="{% url 'import_categories' %}" class="btn btn-outline-success shadow-sm">
<i class="bi bi-file-earmark-arrow-up me-2"></i>{% trans "Import Categories" %}
</a>
<button class="btn btn-outline-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addUnitModal">
<i class="bi bi-rulers me-2"></i>{% trans "Add Unit" %}
</button>

View File

@ -15,9 +15,14 @@
</ol>
</nav>
</div>
<div class="btn-group">
<button class="btn btn-primary shadow-sm" data-bs-toggle="modal" data-bs-target="#addSupplierModal">
<i class="bi bi-person-plus me-2"></i>{% trans "Add Supplier" %}
</button>
<a href="{% url 'import_suppliers' %}" class="btn btn-success shadow-sm">
<i class="bi bi-file-earmark-excel me-2"></i>{% trans "Import" %}
</a>
</div>
</div>
{% if messages %}
@ -60,6 +65,39 @@
</div>
</td>
</tr>
<!-- Edit Supplier Modal -->
<div class="modal fade" id="editSupplierModal{{ supplier.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">{% trans "Edit Supplier" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'edit_supplier' supplier.id %}" method="POST">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Supplier Name" %}</label>
<input type="text" name="name" class="form-control rounded-3" value="{{ supplier.name }}" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Contact Person" %}</label>
<input type="text" name="contact_person" class="form-control rounded-3" value="{{ supplier.contact_person }}">
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Phone Number" %}</label>
<input type="text" name="phone" class="form-control rounded-3" value="{{ supplier.phone }}">
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
</tbody>
</table>

View File

@ -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/<int:pk>/', views.edit_supplier, name='edit_supplier'),
path('suppliers/delete/<int:pk>/', 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/<int:pk>/', views.edit_category, name='edit_category'),
path('inventory/category/delete/<int:pk>/', 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'),

139
core/views_import.py Normal file
View File

@ -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})

43
patch_models_timestamp.py Normal file
View File

@ -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")