diff --git a/accounting/__pycache__/signals.cpython-311.pyc b/accounting/__pycache__/signals.cpython-311.pyc
index 7f8a8b7..9452ace 100644
Binary files a/accounting/__pycache__/signals.cpython-311.pyc and b/accounting/__pycache__/signals.cpython-311.pyc differ
diff --git a/accounting/__pycache__/views.cpython-311.pyc b/accounting/__pycache__/views.cpython-311.pyc
index c4192dc..60fe963 100644
Binary files a/accounting/__pycache__/views.cpython-311.pyc and b/accounting/__pycache__/views.cpython-311.pyc differ
diff --git a/accounting/signals.py b/accounting/signals.py
index 6d0060c..34804c6 100644
--- a/accounting/signals.py
+++ b/accounting/signals.py
@@ -24,22 +24,45 @@ def create_journal_entry(obj, description, items):
if not items:
return None
+ # Filter out items with 0 amount
+ valid_items = []
+ for item in items:
+ try:
+ # Ensure amount is Decimal
+ amount = Decimal(str(item['amount']))
+ if amount > 0:
+ item['amount'] = amount
+ valid_items.append(item)
+ except:
+ continue
+
+ if not valid_items:
+ return None
+
+ # Determine Entry Date
+ entry_date = timezone.now().date()
+ if hasattr(obj, 'date'):
+ entry_date = obj.date
+ elif hasattr(obj, 'payment_date'):
+ entry_date = obj.payment_date
+ elif hasattr(obj, 'created_at'):
+ entry_date = obj.created_at.date()
+
entry = JournalEntry.objects.create(
- date=getattr(obj, 'created_at', timezone.now()).date() if hasattr(obj, 'created_at') else timezone.now().date(),
+ date=entry_date,
description=description,
content_type=content_type,
object_id=obj.id,
reference=f"{obj.__class__.__name__} #{obj.id}"
)
- for item in items:
- if item['amount'] > 0:
- JournalItem.objects.create(
- entry=entry,
- account=item['account'],
- type=item['type'],
- amount=item['amount']
- )
+ for item in valid_items:
+ JournalItem.objects.create(
+ entry=entry,
+ account=item['account'],
+ type=item['type'],
+ amount=item['amount']
+ )
return entry
@receiver(post_save, sender=Sale)
@@ -51,15 +74,10 @@ def sale_accounting_handler(sender, instance, created, **kwargs):
ar_acc = get_account('1200')
sales_acc = get_account('4000')
- vat_acc = get_account('2100')
if not ar_acc or not sales_acc:
return
- # Subtotal and VAT logic (assuming total_amount includes VAT for now as per Sale model simplicity)
- # Actually Sale model has total_amount and discount.
- # Let's assume total_amount is the final amount.
-
items = [
{'account': ar_acc, 'type': 'debit', 'amount': instance.total_amount},
{'account': sales_acc, 'type': 'credit', 'amount': instance.total_amount},
diff --git a/accounting/views.py b/accounting/views.py
index 43f269a..6a563f6 100644
--- a/accounting/views.py
+++ b/accounting/views.py
@@ -4,7 +4,8 @@ from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Account, JournalEntry, JournalItem
from .forms import AccountForm, JournalEntryForm
-from django.db.models import Sum, Q
+from django.db.models import Sum, Q, Value, DecimalField
+from django.db.models.functions import Coalesce
from django.utils import timezone
from datetime import datetime
from django.db import transaction
@@ -70,8 +71,8 @@ def account_create_update(request, pk=None):
@login_required
def journal_entries(request):
entries = JournalEntry.objects.annotate(
- total_debit=Sum('items__amount', filter=Q(items__type='debit')),
- total_credit=Sum('items__amount', filter=Q(items__type='credit'))
+ total_debit=Coalesce(Sum('items__amount', filter=Q(items__type='debit')), Value(0), output_field=DecimalField()),
+ total_credit=Coalesce(Sum('items__amount', filter=Q(items__type='credit')), Value(0), output_field=DecimalField())
).prefetch_related('items__account').order_by('-date', '-id')
return render(request, 'accounting/journal_entries.html', {'entries': entries})
diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index aa96f4c..366bd66 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 081938d..8244ec0 100644
Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ
diff --git a/core/patch_views_sales_list.py b/core/patch_views_sales_list.py
new file mode 100644
index 0000000..7c06679
--- /dev/null
+++ b/core/patch_views_sales_list.py
@@ -0,0 +1,31 @@
+@login_required
+def invoice_list(request):
+ sales = Sale.objects.all().order_by('-created_at')
+
+ # Filter by date range
+ start_date = request.GET.get('start_date')
+ end_date = request.GET.get('end_date')
+ if start_date:
+ sales = sales.filter(created_at__date__gte=start_date)
+ if end_date:
+ sales = sales.filter(created_at__date__lte=end_date)
+
+ # Filter by customer
+ customer_id = request.GET.get('customer')
+ if customer_id:
+ sales = sales.filter(customer_id=customer_id)
+
+ # Filter by status
+ status = request.GET.get('status')
+ if status:
+ sales = sales.filter(status=status)
+
+ paginator = Paginator(sales, 25)
+
+ context = {
+ 'sales': paginator.get_page(request.GET.get('page')),
+ 'customers': Customer.objects.all(),
+ 'payment_methods': PaymentMethod.objects.filter(is_active=True),
+ 'site_settings': SystemSetting.objects.first(),
+ }
+ return render(request, 'core/invoices.html', context)
\ No newline at end of file
diff --git a/core/templates/core/expenses.html b/core/templates/core/expenses.html
index 21558ca..37a448d 100644
--- a/core/templates/core/expenses.html
+++ b/core/templates/core/expenses.html
@@ -117,12 +117,82 @@
{% endif %}
+
+
+
+
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/core/urls.py b/core/urls.py
index a2bade7..045ad69 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -62,6 +62,7 @@ urlpatterns = [
# Expenses
path('expenses/', views.expenses_view, name='expenses'),
path('expenses/create/', views.expense_create_view, name='expense_create'),
+ path('expenses/edit//', views.expense_edit_view, name='expense_edit'),
path('expenses/delete//', views.expense_delete_view, name='expense_delete'),
path('expenses/categories/', views.expense_categories_view, name='expense_categories'),
path('expenses/categories/delete//', views.expense_category_delete_view, name='expense_category_delete'),
diff --git a/core/views.py b/core/views.py
index c121f76..253ed2a 100644
--- a/core/views.py
+++ b/core/views.py
@@ -364,8 +364,34 @@ def cashflow_report(request):
@login_required
def invoice_list(request):
sales = Sale.objects.all().order_by('-created_at')
+
+ # Filter by date range
+ start_date = request.GET.get('start_date')
+ end_date = request.GET.get('end_date')
+ if start_date:
+ sales = sales.filter(created_at__date__gte=start_date)
+ if end_date:
+ sales = sales.filter(created_at__date__lte=end_date)
+
+ # Filter by customer
+ customer_id = request.GET.get('customer')
+ if customer_id:
+ sales = sales.filter(customer_id=customer_id)
+
+ # Filter by status
+ status = request.GET.get('status')
+ if status:
+ sales = sales.filter(status=status)
+
paginator = Paginator(sales, 25)
- return render(request, 'core/invoices.html', {'sales': paginator.get_page(request.GET.get('page'))})
+
+ context = {
+ 'sales': paginator.get_page(request.GET.get('page')),
+ 'customers': Customer.objects.all(),
+ 'payment_methods': PaymentMethod.objects.filter(is_active=True),
+ 'site_settings': SystemSetting.objects.first(),
+ }
+ return render(request, 'core/invoices.html', context)
@login_required
def invoice_detail(request, pk):
@@ -431,7 +457,129 @@ def create_purchase_return_api(request): return JsonResponse({'success': False})
@login_required
def export_expenses_excel(request): return redirect('expenses')
@csrf_exempt
-def update_sale_api(request, pk): return JsonResponse({'success': False})
+def update_sale_api(request, pk):
+ if request.method != 'POST':
+ return JsonResponse({'success': False, 'error': 'Invalid request method'})
+
+ try:
+ sale = Sale.objects.get(pk=pk)
+ data = json.loads(request.body)
+
+ customer_id = data.get('customer_id')
+ items = data.get('items', [])
+ discount = decimal.Decimal(str(data.get('discount', 0)))
+ paid_amount = decimal.Decimal(str(data.get('paid_amount', 0)))
+ payment_type = data.get('payment_type', 'cash')
+ payment_method_id = data.get('payment_method_id')
+ due_date = data.get('due_date')
+ notes = data.get('notes', '')
+ invoice_number = data.get('invoice_number')
+
+ if not items:
+ return JsonResponse({'success': False, 'error': 'No items in sale'})
+
+ with transaction.atomic():
+ # 1. Revert Stock
+ for item in sale.items.all():
+ product = item.product
+ product.stock_quantity += item.quantity
+ product.save()
+
+ # 2. Delete existing items
+ sale.items.all().delete()
+
+ # 3. Update Sale Details
+ if customer_id:
+ sale.customer_id = customer_id
+ else:
+ sale.customer = None
+
+ sale.discount = discount
+ sale.notes = notes
+ if invoice_number:
+ sale.invoice_number = invoice_number
+
+ if due_date:
+ sale.due_date = due_date
+ else:
+ sale.due_date = None
+
+ # 4. Create New Items and Deduct Stock
+ subtotal = decimal.Decimal(0)
+
+ for item_data in items:
+ product = Product.objects.get(pk=item_data['id'])
+ quantity = decimal.Decimal(str(item_data['quantity']))
+ price = decimal.Decimal(str(item_data['price']))
+
+ # Deduct stock
+ product.stock_quantity -= quantity
+ product.save()
+
+ line_total = price * quantity
+ subtotal += line_total
+
+ SaleItem.objects.create(
+ sale=sale,
+ product=product,
+ quantity=qty,
+ unit_price=price,
+ line_total=line_total
+ )
+
+ sale.subtotal = subtotal
+ sale.total_amount = subtotal - discount
+
+ # 5. Handle Payments
+ if payment_type == 'credit':
+ sale.status = 'unpaid'
+ sale.paid_amount = 0
+ sale.balance_due = sale.total_amount
+ sale.payments.all().delete()
+
+ elif payment_type == 'cash':
+ sale.status = 'paid'
+ sale.paid_amount = sale.total_amount
+ sale.balance_due = 0
+
+ sale.payments.all().delete()
+ SalePayment.objects.create(
+ sale=sale,
+ amount=sale.total_amount,
+ payment_method_id=payment_method_id if payment_method_id else None,
+ payment_date=timezone.now().date(),
+ notes='Full Payment (Edit)'
+ )
+
+ elif payment_type == 'partial':
+ sale.paid_amount = paid_amount
+ sale.balance_due = sale.total_amount - paid_amount
+ if sale.balance_due <= 0:
+ sale.status = 'paid'
+ sale.balance_due = 0
+ else:
+ sale.status = 'partial'
+
+ sale.payments.all().delete()
+ SalePayment.objects.create(
+ sale=sale,
+ amount=paid_amount,
+ payment_method_id=payment_method_id if payment_method_id else None,
+ payment_date=timezone.now().date(),
+ notes='Partial Payment (Edit)'
+ )
+
+ sale.save()
+
+ return JsonResponse({'success': True, 'sale_id': sale.id})
+
+ except Sale.DoesNotExist:
+ return JsonResponse({'success': False, 'error': 'Sale not found'})
+ except Product.DoesNotExist:
+ return JsonResponse({'success': False, 'error': 'Product not found'})
+ except Exception as e:
+ return JsonResponse({'success': False, 'error': str(e)})
+
@csrf_exempt
def hold_sale_api(request): return JsonResponse({'success': False})
@csrf_exempt
@@ -524,16 +672,132 @@ def start_session(request): return redirect('cashier_session_list')
def close_session(request): return redirect('cashier_session_list')
@login_required
def session_detail(request, pk): return redirect('cashier_session_list')
+
@login_required
-def expenses_view(request): return render(request, 'core/expenses.html')
+def expenses_view(request):
+ expenses = Expense.objects.all().select_related('category', 'payment_method', 'created_by').order_by('-date')
+ categories = ExpenseCategory.objects.all()
+ payment_methods = PaymentMethod.objects.filter(is_active=True)
+
+ paginator = Paginator(expenses, 25)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
+
+ context = {
+ 'expenses': page_obj,
+ 'categories': categories,
+ 'payment_methods': payment_methods,
+ }
+ return render(request, 'core/expenses.html', context)
+
@login_required
-def expense_create_view(request): return redirect('expenses')
+def expense_create_view(request):
+ if request.method == 'POST':
+ try:
+ category_id = request.POST.get('category')
+ amount = request.POST.get('amount')
+ date = request.POST.get('date')
+ description = request.POST.get('description')
+ payment_method_id = request.POST.get('payment_method')
+
+ category = get_object_or_404(ExpenseCategory, pk=category_id)
+ payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None
+
+ expense = Expense.objects.create(
+ category=category,
+ amount=amount,
+ date=date or timezone.now().date(),
+ description=description,
+ payment_method=payment_method,
+ created_by=request.user
+ )
+
+ if 'attachment' in request.FILES:
+ expense.attachment = request.FILES['attachment']
+ expense.save()
+
+ messages.success(request, _('Expense added successfully.'))
+ except Exception as e:
+ messages.error(request, _('Error adding expense: ') + str(e))
+
+ return redirect('expenses')
+
@login_required
-def expense_delete_view(request, pk): return redirect('expenses')
+def expense_edit_view(request, pk):
+ expense = get_object_or_404(Expense, pk=pk)
+ if request.method == 'POST':
+ try:
+ category_id = request.POST.get('category')
+ amount = request.POST.get('amount')
+ date = request.POST.get('date')
+ description = request.POST.get('description')
+ payment_method_id = request.POST.get('payment_method')
+
+ category = get_object_or_404(ExpenseCategory, pk=category_id)
+ payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None
+
+ expense.category = category
+ expense.amount = amount
+ expense.date = date or expense.date
+ expense.description = description
+ expense.payment_method = payment_method
+
+ if 'attachment' in request.FILES:
+ expense.attachment = request.FILES['attachment']
+
+ expense.save()
+ messages.success(request, _('Expense updated successfully.'))
+ except Exception as e:
+ messages.error(request, _('Error updating expense: ') + str(e))
+
+ return redirect('expenses')
+
@login_required
-def expense_categories_view(request): return render(request, 'core/expense_categories.html')
+def expense_delete_view(request, pk):
+ expense = get_object_or_404(Expense, pk=pk)
+ expense.delete()
+ messages.success(request, _('Expense deleted successfully.'))
+ return redirect('expenses')
+
@login_required
-def expense_category_delete_view(request, pk): return redirect('expense_categories')
+def expense_categories_view(request):
+ if request.method == 'POST':
+ category_id = request.POST.get('category_id')
+ name_en = request.POST.get('name_en')
+ name_ar = request.POST.get('name_ar')
+ description = request.POST.get('description')
+
+ if category_id:
+ # Update existing category
+ category = get_object_or_404(ExpenseCategory, pk=category_id)
+ category.name_en = name_en
+ category.name_ar = name_ar
+ category.description = description
+ category.save()
+ messages.success(request, _('Expense category updated successfully.'))
+ else:
+ # Create new category
+ ExpenseCategory.objects.create(
+ name_en=name_en,
+ name_ar=name_ar,
+ description=description
+ )
+ messages.success(request, _('Expense category added successfully.'))
+ return redirect('expense_categories')
+
+ categories = ExpenseCategory.objects.all().order_by('-id')
+ return render(request, 'core/expense_categories.html', {'categories': categories})
+
+@login_required
+def expense_category_delete_view(request, pk):
+ category = get_object_or_404(ExpenseCategory, pk=pk)
+ if category.expenses.exists():
+ messages.error(request, _('Cannot delete category because it has related expenses.'))
+ else:
+ category.delete()
+ messages.success(request, _('Expense category deleted successfully.'))
+ return redirect('expense_categories')
+
@login_required
def expense_report(request): return render(request, 'core/expense_report.html')
@login_required
@@ -541,9 +805,59 @@ def customer_payments(request): return redirect('invoices')
@login_required
def customer_payment_receipt(request, pk): return redirect('invoices')
@login_required
-def sale_receipt(request, pk): return redirect('invoices')
+def sale_receipt(request, pk):
+ sale = get_object_or_404(Sale, pk=pk)
+ settings = SystemSetting.objects.first()
+ return render(request, 'core/sale_receipt.html', {
+ 'sale': sale,
+ 'settings': settings
+ })
+
@login_required
-def edit_invoice(request, pk): return redirect('invoices')
+def edit_invoice(request, pk):
+ sale = get_object_or_404(Sale, pk=pk)
+ customers = Customer.objects.all()
+ products = Product.objects.filter(is_active=True).select_related('category')
+ payment_methods = PaymentMethod.objects.filter(is_active=True)
+ site_settings = SystemSetting.objects.first()
+
+ decimal_places = 2
+ if site_settings:
+ decimal_places = site_settings.decimal_places
+
+ # Serialize items for Vue
+ cart_items = []
+ for item in sale.items.all().select_related('product'):
+ cart_items.append({
+ 'id': item.product.id,
+ 'name_en': item.product.name_en,
+ 'name_ar': item.product.name_ar,
+ 'sku': item.product.sku,
+ 'price': float(item.unit_price),
+ 'quantity': float(item.quantity),
+ 'stock': float(item.product.stock_quantity)
+ })
+
+ cart_json = json.dumps(cart_items)
+
+ # Get first payment method if exists
+ payment_method_id = ""
+ first_payment = sale.payments.first()
+ if first_payment and first_payment.payment_method:
+ payment_method_id = first_payment.payment_method.id
+
+ context = {
+ 'sale': sale,
+ 'customers': customers,
+ 'products': products,
+ 'payment_methods': payment_methods,
+ 'site_settings': site_settings,
+ 'decimal_places': decimal_places,
+ 'cart_json': cart_json,
+ 'payment_method_id': payment_method_id
+ }
+ return render(request, 'core/invoice_edit.html', context)
+
@login_required
def add_sale_payment(request, pk): return redirect('invoices')
@login_required
diff --git a/debug_accounting.py b/debug_accounting.py
new file mode 100644
index 0000000..1fa0972
--- /dev/null
+++ b/debug_accounting.py
@@ -0,0 +1,31 @@
+import os
+import django
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
+django.setup()
+
+from accounting.models import Account, JournalEntry, JournalItem
+from core.models import Expense
+
+print("Checking Accounts...")
+acc_1000 = Account.objects.filter(code='1000').first()
+acc_5400 = Account.objects.filter(code='5400').first()
+
+print(f"Account 1000 (Cash): {acc_1000}")
+print(f"Account 5400 (General Expense): {acc_5400}")
+
+print("\nChecking Journal Entries for Expenses...")
+expenses = Expense.objects.all()
+for exp in expenses:
+ print(f"Expense {exp.id}: {exp.description} - Amount: {exp.amount}")
+ # Find linked entry
+ from django.contrib.contenttypes.models import ContentType
+ ct = ContentType.objects.get_for_model(Expense)
+ entries = JournalEntry.objects.filter(content_type=ct, object_id=exp.id)
+ for entry in entries:
+ print(f" -> JournalEntry {entry.id}: {entry.description}")
+ items = entry.items.all()
+ if items.exists():
+ for item in items:
+ print(f" -> Item: {item.account.code} {item.type} {item.amount}")
+ else:
+ print(f" -> NO ITEMS FOUND!")
\ No newline at end of file
diff --git a/move_project.py b/move_project.py
new file mode 100644
index 0000000..50a99f4
--- /dev/null
+++ b/move_project.py
@@ -0,0 +1,33 @@
+import os
+import shutil
+
+DEST = 'meezan'
+EXCLUDE = {'.git', '.gemini', DEST, 'move_project.py'}
+
+def move_project():
+ # Ensure destination exists
+ if not os.path.exists(DEST):
+ os.makedirs(DEST)
+ print(f"Created directory: {DEST}")
+
+ # Iterate and move
+ for item in os.listdir('.'):
+ if item in EXCLUDE:
+ continue
+
+ # specific check for .env to avoid errors if it doesn't exist yet but user mentioned it
+ if item == '.env':
+ pass # allow moving .env if it exists
+
+ src = item
+ dst = os.path.join(DEST, item)
+
+ try:
+ print(f"Moving {src} -> {dst}...")
+ shutil.move(src, dst)
+ except Exception as e:
+ print(f"Error moving {src}: {e}")
+
+if __name__ == "__main__":
+ move_project()
+ print("Move complete.")
diff --git a/patch_expense_categories.py b/patch_expense_categories.py
new file mode 100644
index 0000000..dc4c7a3
--- /dev/null
+++ b/patch_expense_categories.py
@@ -0,0 +1,43 @@
+import os
+
+file_path = 'core/views.py'
+search_text = "@login_required\ndef expense_categories_view(request): return render(request, 'core/expense_categories.html')"
+replace_text = """@login_required
+def expense_categories_view(request):
+ if request.method == 'POST':
+ category_id = request.POST.get('category_id')
+ name_en = request.POST.get('name_en')
+ name_ar = request.POST.get('name_ar')
+ description = request.POST.get('description')
+
+ if category_id:
+ # Update existing category
+ category = get_object_or_404(ExpenseCategory, pk=category_id)
+ category.name_en = name_en
+ category.name_ar = name_ar
+ category.description = description
+ category.save()
+ messages.success(request, _('Expense category updated successfully.'))
+ else:
+ # Create new category
+ ExpenseCategory.objects.create(
+ name_en=name_en,
+ name_ar=name_ar,
+ description=description
+ )
+ messages.success(request, _('Expense category added successfully.'))
+ return redirect('expense_categories')
+
+ categories = ExpenseCategory.objects.all().order_by('-id')
+ return render(request, 'core/expense_categories.html', {'categories': categories})"""
+
+with open(file_path, 'r') as f:
+ content = f.read()
+
+if search_text in content:
+ new_content = content.replace(search_text, replace_text)
+ with open(file_path, 'w') as f:
+ f.write(new_content)
+ print("Successfully patched expense_categories_view")
+else:
+ print("Could not find the target function to replace")
diff --git a/patch_invoice_list.py b/patch_invoice_list.py
new file mode 100644
index 0000000..d4acaaa
--- /dev/null
+++ b/patch_invoice_list.py
@@ -0,0 +1,52 @@
+import os
+
+file_path = 'core/views.py'
+
+old_content = """@login_required
+def invoice_list(request):
+ sales = Sale.objects.all().order_by('-created_at')
+ paginator = Paginator(sales, 25)
+ return render(request, 'core/invoices.html', {'sales': paginator.get_page(request.GET.get('page'))})"""
+
+new_content = """@login_required
+def invoice_list(request):
+ sales = Sale.objects.all().order_by('-created_at')
+
+ # Filter by date range
+ start_date = request.GET.get('start_date')
+ end_date = request.GET.get('end_date')
+ if start_date:
+ sales = sales.filter(created_at__date__gte=start_date)
+ if end_date:
+ sales = sales.filter(created_at__date__lte=end_date)
+
+ # Filter by customer
+ customer_id = request.GET.get('customer')
+ if customer_id:
+ sales = sales.filter(customer_id=customer_id)
+
+ # Filter by status
+ status = request.GET.get('status')
+ if status:
+ sales = sales.filter(status=status)
+
+ paginator = Paginator(sales, 25)
+
+ context = {
+ 'sales': paginator.get_page(request.GET.get('page')),
+ 'customers': Customer.objects.all(),
+ 'payment_methods': PaymentMethod.objects.filter(is_active=True),
+ 'site_settings': SystemSetting.objects.first(),
+ }
+ return render(request, 'core/invoices.html', context)"""
+
+with open(file_path, 'r') as f:
+ content = f.read()
+
+if old_content in content:
+ content = content.replace(old_content, new_content)
+ with open(file_path, 'w') as f:
+ f.write(content)
+ print("Successfully patched invoice_list")
+else:
+ print("Could not find exact match for invoice_list function")
diff --git a/patch_views_expense_edit.py b/patch_views_expense_edit.py
new file mode 100644
index 0000000..fd824fd
--- /dev/null
+++ b/patch_views_expense_edit.py
@@ -0,0 +1,30 @@
+
+@login_required
+def expense_edit_view(request, pk):
+ expense = get_object_or_404(Expense, pk=pk)
+ if request.method == 'POST':
+ try:
+ category_id = request.POST.get('category')
+ amount = request.POST.get('amount')
+ date = request.POST.get('date')
+ description = request.POST.get('description')
+ payment_method_id = request.POST.get('payment_method')
+
+ category = get_object_or_404(ExpenseCategory, pk=category_id)
+ payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None
+
+ expense.category = category
+ expense.amount = amount
+ expense.date = date or expense.date
+ expense.description = description
+ expense.payment_method = payment_method
+
+ if 'attachment' in request.FILES:
+ expense.attachment = request.FILES['attachment']
+
+ expense.save()
+ messages.success(request, _('Expense updated successfully.'))
+ except Exception as e:
+ messages.error(request, _('Error updating expense: ') + str(e))
+
+ return redirect('expenses')
diff --git a/patch_views_sales.py b/patch_views_sales.py
new file mode 100644
index 0000000..62ea21c
--- /dev/null
+++ b/patch_views_sales.py
@@ -0,0 +1,206 @@
+import os
+
+file_path = 'core/views.py'
+
+# New Implementations
+edit_invoice_code = """
+@login_required
+def edit_invoice(request, pk):
+ sale = get_object_or_404(Sale, pk=pk)
+ customers = Customer.objects.all()
+ products = Product.objects.filter(is_active=True).select_related('category')
+ payment_methods = PaymentMethod.objects.filter(is_active=True)
+ site_settings = SystemSetting.objects.first()
+
+ decimal_places = 2
+ if site_settings:
+ decimal_places = site_settings.decimal_places
+
+ # Serialize items for Vue
+ cart_items = []
+ for item in sale.items.all().select_related('product'):
+ cart_items.append({
+ 'id': item.product.id,
+ 'name_en': item.product.name_en,
+ 'name_ar': item.product.name_ar,
+ 'sku': item.product.sku,
+ 'price': float(item.unit_price),
+ 'quantity': float(item.quantity),
+ 'stock': float(item.product.stock_quantity)
+ })
+
+ cart_json = json.dumps(cart_items)
+
+ # Get first payment method if exists
+ payment_method_id = ""
+ first_payment = sale.payments.first()
+ if first_payment and first_payment.payment_method:
+ payment_method_id = first_payment.payment_method.id
+
+ context = {
+ 'sale': sale,
+ 'customers': customers,
+ 'products': products,
+ 'payment_methods': payment_methods,
+ 'site_settings': site_settings,
+ 'decimal_places': decimal_places,
+ 'cart_json': cart_json,
+ 'payment_method_id': payment_method_id
+ }
+ return render(request, 'core/invoice_edit.html', context)
+"""
+
+sale_receipt_code = """
+@login_required
+def sale_receipt(request, pk):
+ sale = get_object_or_404(Sale, pk=pk)
+ settings = SystemSetting.objects.first()
+ return render(request, 'core/sale_receipt.html', {
+ 'sale': sale,
+ 'settings': settings
+ })
+"""
+
+update_sale_api_code = """
+@csrf_exempt
+def update_sale_api(request, pk):
+ if request.method != 'POST':
+ return JsonResponse({'success': False, 'error': 'Invalid request method'})
+
+ try:
+ sale = Sale.objects.get(pk=pk)
+ data = json.loads(request.body)
+
+ customer_id = data.get('customer_id')
+ items = data.get('items', [])
+ discount = decimal.Decimal(str(data.get('discount', 0)))
+ paid_amount = decimal.Decimal(str(data.get('paid_amount', 0)))
+ payment_type = data.get('payment_type', 'cash')
+ payment_method_id = data.get('payment_method_id')
+ due_date = data.get('due_date')
+ notes = data.get('notes', '')
+ invoice_number = data.get('invoice_number')
+
+ if not items:
+ return JsonResponse({'success': False, 'error': 'No items in sale'})
+
+ with transaction.atomic():
+ # 1. Revert Stock
+ for item in sale.items.all():
+ product = item.product
+ product.stock_quantity += item.quantity
+ product.save()
+
+ # 2. Delete existing items
+ sale.items.all().delete()
+
+ # 3. Update Sale Details
+ if customer_id:
+ sale.customer_id = customer_id
+ else:
+ sale.customer = None
+
+ sale.discount = discount
+ sale.notes = notes
+ if invoice_number:
+ sale.invoice_number = invoice_number
+
+ if due_date:
+ sale.due_date = due_date
+ else:
+ sale.due_date = None
+
+ # 4. Create New Items and Deduct Stock
+ subtotal = decimal.Decimal(0)
+
+ for item_data in items:
+ product = Product.objects.get(pk=item_data['id'])
+ quantity = decimal.Decimal(str(item_data['quantity']))
+ price = decimal.Decimal(str(item_data['price']))
+
+ # Deduct stock
+ product.stock_quantity -= quantity
+ product.save()
+
+ line_total = price * quantity
+ subtotal += line_total
+
+ SaleItem.objects.create(
+ sale=sale,
+ product=product,
+ quantity=quantity,
+ unit_price=price,
+ line_total=line_total
+ )
+
+ sale.subtotal = subtotal
+ sale.total_amount = subtotal - discount
+
+ # 5. Handle Payments
+ if payment_type == 'credit':
+ sale.status = 'unpaid'
+ sale.paid_amount = 0
+ sale.balance_due = sale.total_amount
+ sale.payments.all().delete()
+
+ elif payment_type == 'cash':
+ sale.status = 'paid'
+ sale.paid_amount = sale.total_amount
+ sale.balance_due = 0
+
+ sale.payments.all().delete()
+ SalePayment.objects.create(
+ sale=sale,
+ amount=sale.total_amount,
+ payment_method_id=payment_method_id if payment_method_id else None,
+ payment_date=timezone.now().date(),
+ notes='Full Payment (Edit)'
+ )
+
+ elif payment_type == 'partial':
+ sale.paid_amount = paid_amount
+ sale.balance_due = sale.total_amount - paid_amount
+ if sale.balance_due <= 0:
+ sale.status = 'paid'
+ sale.balance_due = 0
+ else:
+ sale.status = 'partial'
+
+ sale.payments.all().delete()
+ SalePayment.objects.create(
+ sale=sale,
+ amount=paid_amount,
+ payment_method_id=payment_method_id if payment_method_id else None,
+ payment_date=timezone.now().date(),
+ notes='Partial Payment (Edit)'
+ )
+
+ sale.save()
+
+ return JsonResponse({'success': True, 'sale_id': sale.id})
+
+ except Sale.DoesNotExist:
+ return JsonResponse({'success': False, 'error': 'Sale not found'})
+ except Product.DoesNotExist:
+ return JsonResponse({'success': False, 'error': 'Product not found'})
+ except Exception as e:
+ return JsonResponse({'success': False, 'error': str(e)})
+"""
+
+with open(file_path, 'r') as f:
+ content = f.read()
+
+# Replace stubs
+content = content.replace("def sale_receipt(request, pk): return redirect('invoices')", sale_receipt_code)
+content = content.replace("def edit_invoice(request, pk): return redirect('invoices')", edit_invoice_code)
+content = content.replace("@csrf_exempt\ndef update_sale_api(request, pk): return JsonResponse({'success': False})", update_sale_api_code)
+
+# Handle potential whitespace variations if single-line replace fails
+if "def edit_invoice(request, pk): return redirect('invoices')" in content: # Check if it persisted
+ pass # worked
+else:
+ # Fallback for manual check if needed (it should work given exact match from read_file)
+ pass
+
+with open(file_path, 'w') as f:
+ f.write(content)