update final

This commit is contained in:
Flatlogic Bot 2026-02-08 06:12:20 +00:00
parent 8150c2ba43
commit ee5b4ff280
16 changed files with 857 additions and 27 deletions

View File

@ -24,16 +24,39 @@ 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:
for item in valid_items:
JournalItem.objects.create(
entry=entry,
account=item['account'],
@ -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},

View File

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

View File

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

View File

@ -117,12 +117,82 @@
<i class="bi bi-paperclip"></i>
</a>
{% endif %}
<button class="btn btn-sm btn-light rounded-circle me-1" data-bs-toggle="modal" data-bs-target="#editExpenseModal{{ expense.id }}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light rounded-circle text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ expense.id }}">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<!-- Edit Modal -->
<div class="modal fade" id="editExpenseModal{{ expense.id }}" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 shadow">
<div class="modal-header p-4 border-0">
<h5 class="modal-title fw-bold">{% trans "Edit Expense" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="{% url 'expense_edit' expense.pk %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body p-4 text-start">
<div class="mb-3">
<label class="form-label fw-bold">{% trans "Category" %}</label>
<select name="category" class="form-select rounded-3" required>
<option value="">{% trans "Select Category" %}</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if expense.category.id == category.id %}selected{% endif %}>{{ category.name_en }} / {{ category.name_ar }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label fw-bold">{% trans "Amount" %}</label>
<div class="input-group">
<span class="input-group-text">{{ site_settings.currency_symbol }}</span>
<input type="number" step="0.001" name="amount" class="form-control rounded-end-3" value="{{ expense.amount|stringformat:'f' }}" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">{% trans "Date" %}</label>
<input type="date" name="date" class="form-control rounded-3" value="{{ expense.date|date:'Y-m-d' }}" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">{% trans "Payment Method" %}</label>
<select name="payment_method" class="form-select rounded-3">
<option value="">{% trans "None" %}</option>
{% for pm in payment_methods %}
<option value="{{ pm.id }}" {% if expense.payment_method.id == pm.id %}selected{% endif %}>{{ pm.name_en }} / {{ pm.name_ar }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">{% trans "Description" %}</label>
<textarea name="description" class="form-control rounded-3" rows="3">{{ expense.description|default_if_none:"" }}</textarea>
</div>
<div class="mb-0">
<label class="form-label fw-bold">{% trans "Attachment" %} <small class="text-muted fw-normal">({% trans "Optional" %})</small></label>
{% if expense.attachment %}
<div class="mb-2">
<a href="{{ expense.attachment.url }}" target="_blank" class="small text-decoration-none">
<i class="bi bi-paperclip me-1"></i>{% trans "Current Attachment" %}
</a>
</div>
{% endif %}
<input type="file" name="attachment" class="form-control rounded-3">
</div>
</div>
<div class="modal-footer p-4 border-0">
<button type="button" class="btn btn-light rounded-3 px-4" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary rounded-3 px-4">{% trans "Update Expense" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal{{ expense.id }}" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">

View File

@ -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/<int:pk>/', views.expense_edit_view, name='expense_edit'),
path('expenses/delete/<int:pk>/', views.expense_delete_view, name='expense_delete'),
path('expenses/categories/', views.expense_categories_view, name='expense_categories'),
path('expenses/categories/delete/<int:pk>/', views.expense_category_delete_view, name='expense_category_delete'),

View File

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

31
debug_accounting.py Normal file
View File

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

33
move_project.py Normal file
View File

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

View File

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

52
patch_invoice_list.py Normal file
View File

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

View File

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

206
patch_views_sales.py Normal file
View File

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