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)