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,22 +24,45 @@ def create_journal_entry(obj, description, items):
if not items: if not items:
return None 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( 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, description=description,
content_type=content_type, content_type=content_type,
object_id=obj.id, object_id=obj.id,
reference=f"{obj.__class__.__name__} #{obj.id}" reference=f"{obj.__class__.__name__} #{obj.id}"
) )
for item in items: for item in valid_items:
if item['amount'] > 0: JournalItem.objects.create(
JournalItem.objects.create( entry=entry,
entry=entry, account=item['account'],
account=item['account'], type=item['type'],
type=item['type'], amount=item['amount']
amount=item['amount'] )
)
return entry return entry
@receiver(post_save, sender=Sale) @receiver(post_save, sender=Sale)
@ -51,15 +74,10 @@ def sale_accounting_handler(sender, instance, created, **kwargs):
ar_acc = get_account('1200') ar_acc = get_account('1200')
sales_acc = get_account('4000') sales_acc = get_account('4000')
vat_acc = get_account('2100')
if not ar_acc or not sales_acc: if not ar_acc or not sales_acc:
return 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 = [ items = [
{'account': ar_acc, 'type': 'debit', 'amount': instance.total_amount}, {'account': ar_acc, 'type': 'debit', 'amount': instance.total_amount},
{'account': sales_acc, 'type': 'credit', '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 django.contrib import messages
from .models import Account, JournalEntry, JournalItem from .models import Account, JournalEntry, JournalItem
from .forms import AccountForm, JournalEntryForm 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 django.utils import timezone
from datetime import datetime from datetime import datetime
from django.db import transaction from django.db import transaction
@ -70,8 +71,8 @@ def account_create_update(request, pk=None):
@login_required @login_required
def journal_entries(request): def journal_entries(request):
entries = JournalEntry.objects.annotate( entries = JournalEntry.objects.annotate(
total_debit=Sum('items__amount', filter=Q(items__type='debit')), total_debit=Coalesce(Sum('items__amount', filter=Q(items__type='debit')), Value(0), output_field=DecimalField()),
total_credit=Sum('items__amount', filter=Q(items__type='credit')) total_credit=Coalesce(Sum('items__amount', filter=Q(items__type='credit')), Value(0), output_field=DecimalField())
).prefetch_related('items__account').order_by('-date', '-id') ).prefetch_related('items__account').order_by('-date', '-id')
return render(request, 'accounting/journal_entries.html', {'entries': entries}) 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> <i class="bi bi-paperclip"></i>
</a> </a>
{% endif %} {% 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 }}"> <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> <i class="bi bi-trash"></i>
</button> </button>
</td> </td>
</tr> </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 --> <!-- Delete Modal -->
<div class="modal fade" id="deleteModal{{ expense.id }}" tabindex="-1"> <div class="modal fade" id="deleteModal{{ expense.id }}" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">

View File

@ -62,6 +62,7 @@ urlpatterns = [
# Expenses # Expenses
path('expenses/', views.expenses_view, name='expenses'), path('expenses/', views.expenses_view, name='expenses'),
path('expenses/create/', views.expense_create_view, name='expense_create'), 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/delete/<int:pk>/', views.expense_delete_view, name='expense_delete'),
path('expenses/categories/', views.expense_categories_view, name='expense_categories'), 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'), 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 @login_required
def invoice_list(request): def invoice_list(request):
sales = Sale.objects.all().order_by('-created_at') 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) 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 @login_required
def invoice_detail(request, pk): def invoice_detail(request, pk):
@ -431,7 +457,129 @@ def create_purchase_return_api(request): return JsonResponse({'success': False})
@login_required @login_required
def export_expenses_excel(request): return redirect('expenses') def export_expenses_excel(request): return redirect('expenses')
@csrf_exempt @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 @csrf_exempt
def hold_sale_api(request): return JsonResponse({'success': False}) def hold_sale_api(request): return JsonResponse({'success': False})
@csrf_exempt @csrf_exempt
@ -524,16 +672,132 @@ def start_session(request): return redirect('cashier_session_list')
def close_session(request): return redirect('cashier_session_list') def close_session(request): return redirect('cashier_session_list')
@login_required @login_required
def session_detail(request, pk): return redirect('cashier_session_list') def session_detail(request, pk): return redirect('cashier_session_list')
@login_required @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 @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 @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 @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 @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 @login_required
def expense_report(request): return render(request, 'core/expense_report.html') def expense_report(request): return render(request, 'core/expense_report.html')
@login_required @login_required
@ -541,9 +805,59 @@ def customer_payments(request): return redirect('invoices')
@login_required @login_required
def customer_payment_receipt(request, pk): return redirect('invoices') def customer_payment_receipt(request, pk): return redirect('invoices')
@login_required @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 @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 @login_required
def add_sale_payment(request, pk): return redirect('invoices') def add_sale_payment(request, pk): return redirect('invoices')
@login_required @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)