Autosave: 20260208-165737
This commit is contained in:
parent
ee5b4ff280
commit
89eb33ae77
Binary file not shown.
@ -156,23 +156,11 @@ USE_I18N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Script Name (for subpath deployment)
|
|
||||||
FORCE_SCRIPT_NAME = os.getenv("FORCE_SCRIPT_NAME")
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
# Default to 'static/' but allow override or adjust based on FORCE_SCRIPT_NAME
|
STATIC_URL = os.getenv("STATIC_URL", "/static/")
|
||||||
if FORCE_SCRIPT_NAME:
|
MEDIA_URL = os.getenv("MEDIA_URL", "/media/")
|
||||||
# Ensure FORCE_SCRIPT_NAME starts with / and ends without / for consistency if needed,
|
|
||||||
# but normally users provide "/meezan".
|
|
||||||
# We strip trailing slash from script name when forming static url
|
|
||||||
_script_prefix = FORCE_SCRIPT_NAME.rstrip('/')
|
|
||||||
STATIC_URL = os.getenv("STATIC_URL", f"{_script_prefix}/static/")
|
|
||||||
MEDIA_URL = os.getenv("MEDIA_URL", f"{_script_prefix}/media/")
|
|
||||||
else:
|
|
||||||
STATIC_URL = os.getenv("STATIC_URL", "static/")
|
|
||||||
MEDIA_URL = os.getenv("MEDIA_URL", "media/")
|
|
||||||
|
|
||||||
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|||||||
BIN
core/__pycache__/helpers.cpython-311.pyc
Normal file
BIN
core/__pycache__/helpers.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
122
core/helpers.py
Normal file
122
core/helpers.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
def number_to_words_en(number):
|
||||||
|
"""
|
||||||
|
Converts a number to English words.
|
||||||
|
Handles decimals up to 3 places.
|
||||||
|
"""
|
||||||
|
if number == 0:
|
||||||
|
return "Zero"
|
||||||
|
|
||||||
|
units = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten",
|
||||||
|
"Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"]
|
||||||
|
tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"]
|
||||||
|
thousands = ["", "Thousand", "Million", "Billion"]
|
||||||
|
|
||||||
|
def _convert_less_than_thousand(num):
|
||||||
|
res = ""
|
||||||
|
if num >= 100:
|
||||||
|
res += units[num // 100] + " Hundred "
|
||||||
|
num %= 100
|
||||||
|
if num >= 20:
|
||||||
|
res += tens[num // 10] + " "
|
||||||
|
num %= 10
|
||||||
|
if num > 0:
|
||||||
|
res += units[num]
|
||||||
|
return res.strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = str(float(number)).split('.')
|
||||||
|
integer_part = int(parts[0])
|
||||||
|
fractional_part = int(parts[1]) if len(parts) > 1 else 0
|
||||||
|
except ValueError:
|
||||||
|
return "Invalid Number"
|
||||||
|
|
||||||
|
res = ""
|
||||||
|
if integer_part == 0:
|
||||||
|
res = "Zero"
|
||||||
|
else:
|
||||||
|
idx = 0
|
||||||
|
while integer_part > 0:
|
||||||
|
if integer_part % 1000 != 0:
|
||||||
|
res = _convert_less_than_thousand(integer_part % 1000) + " " + thousands[idx] + " " + res
|
||||||
|
integer_part //= 1000
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
words = res.strip()
|
||||||
|
|
||||||
|
if fractional_part > 0:
|
||||||
|
frac_str = parts[1]
|
||||||
|
denom = 10 ** len(frac_str)
|
||||||
|
words += f" and {fractional_part}/{denom}"
|
||||||
|
|
||||||
|
return words
|
||||||
|
|
||||||
|
def number_to_words_ar(number):
|
||||||
|
return number_to_words_en(number)
|
||||||
|
|
||||||
|
def send_whatsapp_message(phone, message):
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
from .models import SystemSetting
|
||||||
|
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
if not settings or not settings.wablas_enabled:
|
||||||
|
return False, "WhatsApp gateway is disabled."
|
||||||
|
|
||||||
|
if not settings.wablas_token or not settings.wablas_server_url:
|
||||||
|
return False, "Wablas configuration is incomplete."
|
||||||
|
|
||||||
|
phone = ''.join(filter(str.isdigit, str(phone)))
|
||||||
|
server_url = settings.wablas_server_url.rstrip('/')
|
||||||
|
url = f"{server_url}/api/send-message"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": settings.wablas_token,
|
||||||
|
"Secret": settings.wablas_secret_key
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"phone": phone, "message": message}
|
||||||
|
|
||||||
|
response = requests.post(url, data=payload, headers=headers, timeout=10)
|
||||||
|
data = response.json()
|
||||||
|
if response.status_code == 200 and data.get('status') == True:
|
||||||
|
return True, "Message sent successfully."
|
||||||
|
else:
|
||||||
|
return False, data.get('message', 'Unknown error from Wablas.')
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
def send_whatsapp_document(phone, document_url, caption=""):
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
from .models import SystemSetting
|
||||||
|
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
if not settings or not settings.wablas_enabled:
|
||||||
|
return False, "WhatsApp gateway is disabled."
|
||||||
|
|
||||||
|
if not settings.wablas_token or not settings.wablas_server_url:
|
||||||
|
return False, "Wablas configuration is incomplete."
|
||||||
|
|
||||||
|
phone = ''.join(filter(str.isdigit, str(phone)))
|
||||||
|
server_url = settings.wablas_server_url.rstrip('/')
|
||||||
|
url = f"{server_url}/api/send-document"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": settings.wablas_token,
|
||||||
|
"Secret": settings.wablas_secret_key
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"phone": phone,
|
||||||
|
"document": document_url,
|
||||||
|
"caption": caption
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, data=payload, headers=headers, timeout=15)
|
||||||
|
data = response.json()
|
||||||
|
if response.status_code == 200 and data.get('status') == True:
|
||||||
|
return True, "Document sent successfully."
|
||||||
|
else:
|
||||||
|
return False, data.get('message', 'Unknown error from Wablas.')
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
299
core/templates/core/purchase_edit.html
Normal file
299
core/templates/core/purchase_edit.html
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n l10n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Edit Purchase" %} | {{ site_settings.business_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid px-4" id="purchaseApp">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Main Form -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-header bg-white border-0 pt-4 px-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="fw-bold mb-0"><i class="bi bi-pencil-square me-2 text-primary"></i>{% trans "Edit Purchase Invoice" %} #[[ invoiceNumber || '{{ purchase.id }}' ]]</h5>
|
||||||
|
<a href="{% url 'purchases' %}" class="btn btn-sm btn-light rounded-pill px-3">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i> {% trans "Back to List" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<!-- Supplier & Invoice Info -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
|
||||||
|
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="supplierId">
|
||||||
|
<option value="">{% trans "Select Supplier" %}</option>
|
||||||
|
{% for supplier in suppliers %}
|
||||||
|
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Invoice #" %}</label>
|
||||||
|
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="invoiceNumber" placeholder="{% trans 'e.g. INV-1001' %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Item Selection -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Search Products" %}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-light border-end-0 border-secondary-subtle"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" class="form-control rounded-3 border-start-0 border-secondary-subtle shadow-none" placeholder="{% trans 'Search by Name or SKU...' %}" v-model="searchQuery" @input="filterProducts">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="position-relative">
|
||||||
|
<div class="list-group position-absolute w-100 shadow rounded-3 mt-1" style="z-index: 1000;" v-if="filteredProducts.length > 0">
|
||||||
|
<button v-for="product in filteredProducts" :key="product.id" class="list-group-item list-group-item-action border-0 py-3" @click="addItem(product)">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<span class="fw-bold">[[ product.name_en ]]</span> / [[ product.name_ar ]]
|
||||||
|
<div class="text-muted small">SKU: [[ product.sku ]] | Stock: [[ product.stock ]]</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-primary fw-bold">[[ currencySymbol ]][[ product.cost_price ]]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items Table -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle">
|
||||||
|
<thead class="bg-light-subtle">
|
||||||
|
<tr class="small text-uppercase text-muted fw-bold">
|
||||||
|
<th style="width: 40%;">{% trans "Product" %}</th>
|
||||||
|
<th class="text-center">{% trans "Cost Price" %}</th>
|
||||||
|
<th class="text-center" style="width: 15%;">{% trans "Quantity" %}</th>
|
||||||
|
<th class="text-end">{% trans "Total" %}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(item, index) in cart" :key="index">
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold">[[ item.name_en ]]</div>
|
||||||
|
<div class="text-muted small">[[ item.sku ]]</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.cost_price" @input="calculateTotal">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal">
|
||||||
|
</td>
|
||||||
|
<td class="text-end fw-bold">[[ currencySymbol ]][[ (parseFloat(item.cost_price) * parseFloat(item.quantity)).toFixed(decimalPlaces) ]]</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="cart.length === 0">
|
||||||
|
<td colspan="5" class="text-center py-5 text-muted">
|
||||||
|
{% trans "Search and add products to this purchase." %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Purchase Summary -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 sticky-top" style="top: 20px;">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h5 class="fw-bold mb-4">{% trans "Purchase Summary" %}</h5>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">{% trans "Total Amount" %}</span>
|
||||||
|
<span class="fw-bold">[[ currencySymbol ]][[ grandTotal.toFixed(decimalPlaces) ]]</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mb-4">
|
||||||
|
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
||||||
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ grandTotal.toFixed(decimalPlaces) ]]</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payment Details -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Payment Type" %}</label>
|
||||||
|
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="paymentType">
|
||||||
|
<option value="cash">{% trans "Cash (Full)" %}</option>
|
||||||
|
<option value="credit">{% trans "Credit (Unpaid)" %}</option>
|
||||||
|
<option value="partial">{% trans "Partial Payment" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" v-if="paymentType !== 'credit'">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Payment Method" %}</label>
|
||||||
|
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="paymentMethodId">
|
||||||
|
{% for method in payment_methods %}
|
||||||
|
<option value="{{ method.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ method.name_ar }}{% else %}{{ method.name_en }}{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" v-if="paymentType === 'partial'">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Paid Amount" %}</label>
|
||||||
|
<input type="number" step="0.001" class="form-control rounded-3" v-model="paidAmount">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4" v-if="paymentType !== 'cash'">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Due Date" %}</label>
|
||||||
|
<input type="date" class="form-control rounded-3" v-model="dueDate">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Internal Notes" %}</label>
|
||||||
|
<textarea class="form-control rounded-3" rows="2" v-model="notes"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button class="btn btn-primary rounded-3 py-3 fw-bold shadow-sm" :disabled="isProcessing || cart.length === 0" @click="savePurchase">
|
||||||
|
<span v-if="isProcessing" class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
<i class="bi bi-check-circle me-2" v-else></i>
|
||||||
|
{% trans "Update Purchase" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vue.js 3 -->
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
{% localize off %}
|
||||||
|
<script>
|
||||||
|
const { createApp } = Vue;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
products: [
|
||||||
|
{% for p in products %}
|
||||||
|
{
|
||||||
|
id: {{ p.id|default:0 }},
|
||||||
|
name_en: "{{ p.name_en|escapejs }}",
|
||||||
|
name_ar: "{{ p.name_ar|escapejs }}",
|
||||||
|
sku: "{{ p.sku|escapejs }}",
|
||||||
|
cost_price: {{ p.cost_price|default:0 }},
|
||||||
|
stock: {{ p.stock_quantity|default:0 }}
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
searchQuery: '',
|
||||||
|
filteredProducts: [],
|
||||||
|
cart: {{ cart_json|safe }},
|
||||||
|
supplierId: '{{ purchase.supplier_id|default:"" }}',
|
||||||
|
invoiceNumber: '{{ purchase.invoice_number|escapejs|default:"" }}',
|
||||||
|
paymentType: '{{ purchase.payment_type|escapejs }}',
|
||||||
|
paymentMethodId: '{{ payment_method_id|default:"" }}',
|
||||||
|
paidAmount: {{ purchase.paid_amount|default:0 }},
|
||||||
|
dueDate: '{{ purchase.due_date|date:"Y-m-d" }}',
|
||||||
|
notes: `{{ purchase.notes|escapejs }}`,
|
||||||
|
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
|
||||||
|
decimalPlaces: {{ decimal_places|default:3 }},
|
||||||
|
isProcessing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
grandTotal() {
|
||||||
|
return this.cart.reduce((total, item) => total + (parseFloat(item.cost_price) * parseFloat(item.quantity)), 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
calculateTotal() {
|
||||||
|
// Computed properties handle this
|
||||||
|
},
|
||||||
|
filterProducts() {
|
||||||
|
if (this.searchQuery.length > 1) {
|
||||||
|
const query = this.searchQuery.toLowerCase();
|
||||||
|
this.filteredProducts = this.products.filter(p =>
|
||||||
|
p.name_en.toLowerCase().includes(query) ||
|
||||||
|
p.sku.toLowerCase().includes(query) ||
|
||||||
|
p.name_ar.includes(query)
|
||||||
|
).slice(0, 5);
|
||||||
|
} else {
|
||||||
|
this.filteredProducts = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addItem(product) {
|
||||||
|
const existing = this.cart.find(item => item.id === product.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.quantity++;
|
||||||
|
} else {
|
||||||
|
this.cart.push({
|
||||||
|
id: product.id,
|
||||||
|
name_en: product.name_en,
|
||||||
|
sku: product.sku,
|
||||||
|
cost_price: product.cost_price,
|
||||||
|
quantity: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.filteredProducts = [];
|
||||||
|
},
|
||||||
|
removeItem(index) {
|
||||||
|
this.cart.splice(index, 1);
|
||||||
|
},
|
||||||
|
savePurchase() {
|
||||||
|
if (this.isProcessing) return;
|
||||||
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
let actualPaidAmount = 0;
|
||||||
|
if (this.paymentType === 'cash') {
|
||||||
|
actualPaidAmount = this.grandTotal;
|
||||||
|
} else if (this.paymentType === 'partial') {
|
||||||
|
actualPaidAmount = this.paidAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
supplier_id: this.supplierId,
|
||||||
|
invoice_number: this.invoiceNumber,
|
||||||
|
items: this.cart.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
cost_price: item.cost_price,
|
||||||
|
line_total: item.cost_price * item.quantity
|
||||||
|
})),
|
||||||
|
total_amount: this.grandTotal,
|
||||||
|
paid_amount: actualPaidAmount,
|
||||||
|
payment_type: this.paymentType,
|
||||||
|
payment_method_id: this.paymentMethodId,
|
||||||
|
due_date: this.dueDate,
|
||||||
|
notes: this.notes
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("{% url 'update_purchase_api' purchase.id %}", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.location.href = "{% url 'purchase_detail' purchase.id %}";
|
||||||
|
} else {
|
||||||
|
alert("Error: " + data.error);
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert("An unexpected error occurred.");
|
||||||
|
this.isProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#purchaseApp');
|
||||||
|
</script>
|
||||||
|
{% endlocalize %}
|
||||||
|
{% endblock %}
|
||||||
@ -69,6 +69,9 @@
|
|||||||
<a href="{% url 'purchase_detail' purchase.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
|
<a href="{% url 'purchase_detail' purchase.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
|
||||||
<i class="bi bi-printer"></i>
|
<i class="bi bi-printer"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'edit_purchase' purchase.id %}" class="btn btn-sm btn-white border text-primary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
{% if purchase.balance_due > 0 %}
|
{% if purchase.balance_due > 0 %}
|
||||||
<button type="button" class="btn btn-sm btn-white border" data-bs-toggle="modal" data-bs-target="#paymentModal{{ purchase.id }}" title="{% trans 'Add Payment' %}">
|
<button type="button" class="btn btn-sm btn-white border" data-bs-toggle="modal" data-bs-target="#paymentModal{{ purchase.id }}" title="{% trans 'Add Payment' %}">
|
||||||
<i class="bi bi-cash-stack"></i>
|
<i class="bi bi-cash-stack"></i>
|
||||||
|
|||||||
@ -48,6 +48,7 @@ urlpatterns = [
|
|||||||
# Purchases (Invoices)
|
# Purchases (Invoices)
|
||||||
path('purchases/create/', views.purchase_create, name='purchase_create'),
|
path('purchases/create/', views.purchase_create, name='purchase_create'),
|
||||||
path('purchases/<int:pk>/', views.purchase_detail, name='purchase_detail'),
|
path('purchases/<int:pk>/', views.purchase_detail, name='purchase_detail'),
|
||||||
|
path('purchases/edit/<int:pk>/', views.edit_purchase, name='edit_purchase'),
|
||||||
path('purchases/payment/<int:pk>/', views.add_purchase_payment, name='add_purchase_payment'),
|
path('purchases/payment/<int:pk>/', views.add_purchase_payment, name='add_purchase_payment'),
|
||||||
path('purchases/delete/<int:pk>/', views.delete_purchase, name='delete_purchase'),
|
path('purchases/delete/<int:pk>/', views.delete_purchase, name='delete_purchase'),
|
||||||
path('purchases/payments/', views.supplier_payments, name='supplier_payments'),
|
path('purchases/payments/', views.supplier_payments, name='supplier_payments'),
|
||||||
@ -77,6 +78,7 @@ urlpatterns = [
|
|||||||
path('api/create-sale/', views.create_sale_api, name='create_sale_api'),
|
path('api/create-sale/', views.create_sale_api, name='create_sale_api'),
|
||||||
path('api/update-sale/<int:pk>/', views.update_sale_api, name='update_sale_api'),
|
path('api/update-sale/<int:pk>/', views.update_sale_api, name='update_sale_api'),
|
||||||
path('api/create-purchase/', views.create_purchase_api, name='create_purchase_api'),
|
path('api/create-purchase/', views.create_purchase_api, name='create_purchase_api'),
|
||||||
|
path('api/update-purchase/<int:pk>/', views.update_purchase_api, name='update_purchase_api'),
|
||||||
|
|
||||||
# POS Held Sales
|
# POS Held Sales
|
||||||
path('api/hold-sale/', views.hold_sale_api, name='hold_sale_api'),
|
path('api/hold-sale/', views.hold_sale_api, name='hold_sale_api'),
|
||||||
|
|||||||
139
core/utils.py
139
core/utils.py
@ -1,7 +1,10 @@
|
|||||||
|
# Copied from core/helpers.py to ensure backward compatibility and safety
|
||||||
|
# This file is deprecated in favor of core/helpers.py but kept for safety.
|
||||||
|
|
||||||
def number_to_words_en(number):
|
def number_to_words_en(number):
|
||||||
"""
|
"""
|
||||||
Converts a number to English words.
|
Converts a number to English words.
|
||||||
Handles decimals up to 3 places (common for some currencies).
|
Handles decimals up to 3 places.
|
||||||
"""
|
"""
|
||||||
if number == 0:
|
if number == 0:
|
||||||
return "Zero"
|
return "Zero"
|
||||||
@ -23,12 +26,13 @@ def number_to_words_en(number):
|
|||||||
res += units[num]
|
res += units[num]
|
||||||
return res.strip()
|
return res.strip()
|
||||||
|
|
||||||
# Split into integer and fractional parts
|
try:
|
||||||
parts = str(float(number)).split('.')
|
parts = str(float(number)).split('.')
|
||||||
integer_part = int(parts[0])
|
integer_part = int(parts[0])
|
||||||
fractional_part = int(parts[1]) if len(parts) > 1 else 0
|
fractional_part = int(parts[1]) if len(parts) > 1 else 0
|
||||||
|
except ValueError:
|
||||||
|
return "Invalid Number"
|
||||||
|
|
||||||
# Convert integer part
|
|
||||||
res = ""
|
res = ""
|
||||||
if integer_part == 0:
|
if integer_part == 0:
|
||||||
res = "Zero"
|
res = "Zero"
|
||||||
@ -42,10 +46,7 @@ def number_to_words_en(number):
|
|||||||
|
|
||||||
words = res.strip()
|
words = res.strip()
|
||||||
|
|
||||||
# Convert fractional part (e.g., for 0.125 -> 125/1000)
|
|
||||||
if fractional_part > 0:
|
if fractional_part > 0:
|
||||||
# Standard way is often "and X/100" or "and X cents"
|
|
||||||
# We'll just append "and X/1000" or similar based on length
|
|
||||||
frac_str = parts[1]
|
frac_str = parts[1]
|
||||||
denom = 10 ** len(frac_str)
|
denom = 10 ** len(frac_str)
|
||||||
words += f" and {fractional_part}/{denom}"
|
words += f" and {fractional_part}/{denom}"
|
||||||
@ -53,49 +54,31 @@ def number_to_words_en(number):
|
|||||||
return words
|
return words
|
||||||
|
|
||||||
def number_to_words_ar(number):
|
def number_to_words_ar(number):
|
||||||
"""
|
return number_to_words_en(number)
|
||||||
A very basic Arabic number to words converter.
|
|
||||||
For a production system, a library like 'num2words' with lang='ar' is highly recommended.
|
|
||||||
"""
|
|
||||||
# This is a placeholder for Arabic. For now, we'll return the English version or just a simplified one.
|
|
||||||
# Since writing a full Arabic number-to-words engine is complex, I'll stick to a simpler implementation
|
|
||||||
# if I can, or just use English for both if not specified.
|
|
||||||
# However, I'll try to provide a basic one if possible.
|
|
||||||
return number_to_words_en(number) # Fallback to EN for now to ensure it works.
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
def send_whatsapp_message(phone, message):
|
def send_whatsapp_message(phone, message):
|
||||||
"""
|
|
||||||
Sends a WhatsApp message via Wablas gateway.
|
|
||||||
"""
|
|
||||||
from .models import SystemSetting
|
|
||||||
settings = SystemSetting.objects.first()
|
|
||||||
|
|
||||||
if not settings or not settings.wablas_enabled:
|
|
||||||
return False, "WhatsApp gateway is disabled."
|
|
||||||
|
|
||||||
if not settings.wablas_token or not settings.wablas_server_url:
|
|
||||||
return False, "Wablas configuration is incomplete."
|
|
||||||
|
|
||||||
# Clean phone number (remove non-digits)
|
|
||||||
phone = ''.join(filter(str.isdigit, str(phone)))
|
|
||||||
|
|
||||||
# Ensure URL is properly formatted
|
|
||||||
server_url = settings.wablas_server_url.rstrip('/')
|
|
||||||
url = f"{server_url}/api/send-message"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": settings.wablas_token,
|
|
||||||
"Secret": settings.wablas_secret_key
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"phone": phone,
|
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
import requests
|
||||||
|
from .models import SystemSetting
|
||||||
|
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
if not settings or not settings.wablas_enabled:
|
||||||
|
return False, "WhatsApp gateway is disabled."
|
||||||
|
|
||||||
|
if not settings.wablas_token or not settings.wablas_server_url:
|
||||||
|
return False, "Wablas configuration is incomplete."
|
||||||
|
|
||||||
|
phone = ''.join(filter(str.isdigit, str(phone)))
|
||||||
|
server_url = settings.wablas_server_url.rstrip('/')
|
||||||
|
url = f"{server_url}/api/send-message"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": settings.wablas_token,
|
||||||
|
"Secret": settings.wablas_secret_key
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {"phone": phone, "message": message}
|
||||||
|
|
||||||
response = requests.post(url, data=payload, headers=headers, timeout=10)
|
response = requests.post(url, data=payload, headers=headers, timeout=10)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if response.status_code == 200 and data.get('status') == True:
|
if response.status_code == 200 and data.get('status') == True:
|
||||||
@ -106,38 +89,32 @@ def send_whatsapp_message(phone, message):
|
|||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
def send_whatsapp_document(phone, document_url, caption=""):
|
def send_whatsapp_document(phone, document_url, caption=""):
|
||||||
"""
|
|
||||||
Sends a document via WhatsApp using Wablas gateway.
|
|
||||||
document_url should be a public URL to the file.
|
|
||||||
"""
|
|
||||||
from .models import SystemSetting
|
|
||||||
settings = SystemSetting.objects.first()
|
|
||||||
|
|
||||||
if not settings or not settings.wablas_enabled:
|
|
||||||
return False, "WhatsApp gateway is disabled."
|
|
||||||
|
|
||||||
if not settings.wablas_token or not settings.wablas_server_url:
|
|
||||||
return False, "Wablas configuration is incomplete."
|
|
||||||
|
|
||||||
# Clean phone number (remove non-digits)
|
|
||||||
phone = ''.join(filter(str.isdigit, str(phone)))
|
|
||||||
|
|
||||||
# Ensure URL is properly formatted
|
|
||||||
server_url = settings.wablas_server_url.rstrip('/')
|
|
||||||
url = f"{server_url}/api/send-document"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": settings.wablas_token,
|
|
||||||
"Secret": settings.wablas_secret_key
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"phone": phone,
|
|
||||||
"document": document_url,
|
|
||||||
"caption": caption
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
import requests
|
||||||
|
from .models import SystemSetting
|
||||||
|
|
||||||
|
settings = SystemSetting.objects.first()
|
||||||
|
if not settings or not settings.wablas_enabled:
|
||||||
|
return False, "WhatsApp gateway is disabled."
|
||||||
|
|
||||||
|
if not settings.wablas_token or not settings.wablas_server_url:
|
||||||
|
return False, "Wablas configuration is incomplete."
|
||||||
|
|
||||||
|
phone = ''.join(filter(str.isdigit, str(phone)))
|
||||||
|
server_url = settings.wablas_server_url.rstrip('/')
|
||||||
|
url = f"{server_url}/api/send-document"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": settings.wablas_token,
|
||||||
|
"Secret": settings.wablas_secret_key
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"phone": phone,
|
||||||
|
"document": document_url,
|
||||||
|
"caption": caption
|
||||||
|
}
|
||||||
|
|
||||||
response = requests.post(url, data=payload, headers=headers, timeout=15)
|
response = requests.post(url, data=payload, headers=headers, timeout=15)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if response.status_code == 200 and data.get('status') == True:
|
if response.status_code == 200 and data.get('status') == True:
|
||||||
@ -145,4 +122,4 @@ def send_whatsapp_document(phone, document_url, caption=""):
|
|||||||
else:
|
else:
|
||||||
return False, data.get('message', 'Unknown error from Wablas.')
|
return False, data.get('message', 'Unknown error from Wablas.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
192
core/views.py
192
core/views.py
@ -9,7 +9,8 @@ import os
|
|||||||
from django.conf import settings as django_settings
|
from django.conf import settings as django_settings
|
||||||
from django.utils.translation import gettext as _, get_language
|
from django.utils.translation import gettext as _, get_language
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from .utils import number_to_words_en, send_whatsapp_document
|
# Changed to use helpers to avoid circular imports and requests issue
|
||||||
|
from .helpers import number_to_words_en, send_whatsapp_document
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
import decimal
|
import decimal
|
||||||
from django.contrib.auth.models import User, Group, Permission
|
from django.contrib.auth.models import User, Group, Permission
|
||||||
@ -40,6 +41,9 @@ from django.utils.text import slugify
|
|||||||
import openpyxl
|
import openpyxl
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
|
# Forced update to trigger reload
|
||||||
|
# Fixed imports to use helpers
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
"""
|
"""
|
||||||
@ -58,10 +62,10 @@ def index(request):
|
|||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
six_months_ago = today - timedelta(days=180)
|
six_months_ago = today - timedelta(days=180)
|
||||||
|
|
||||||
monthly_sales = Sale.objects.filter(created_at__date__gte=six_months_ago)\
|
monthly_sales = Sale.objects.filter(created_at__date__gte=six_months_ago) \
|
||||||
.annotate(month=TruncMonth('created_at'))\
|
.annotate(month=TruncMonth('created_at')) \
|
||||||
.values('month')\
|
.values('month') \
|
||||||
.annotate(total=Sum('total_amount'))\
|
.annotate(total=Sum('total_amount')) \
|
||||||
.order_by('month')
|
.order_by('month')
|
||||||
|
|
||||||
monthly_labels = []
|
monthly_labels = []
|
||||||
@ -77,10 +81,10 @@ def index(request):
|
|||||||
|
|
||||||
# 2. Daily Sales (Last 7 days)
|
# 2. Daily Sales (Last 7 days)
|
||||||
last_week = today - timedelta(days=7)
|
last_week = today - timedelta(days=7)
|
||||||
daily_sales = Sale.objects.filter(created_at__date__gte=last_week)\
|
daily_sales = Sale.objects.filter(created_at__date__gte=last_week) \
|
||||||
.annotate(day=TruncDate('created_at'))\
|
.annotate(day=TruncDate('created_at')) \
|
||||||
.values('day')\
|
.values('day') \
|
||||||
.annotate(total=Sum('total_amount'))\
|
.annotate(total=Sum('total_amount')) \
|
||||||
.order_by('day')
|
.order_by('day')
|
||||||
|
|
||||||
chart_labels = []
|
chart_labels = []
|
||||||
@ -522,7 +526,7 @@ def update_sale_api(request, pk):
|
|||||||
SaleItem.objects.create(
|
SaleItem.objects.create(
|
||||||
sale=sale,
|
sale=sale,
|
||||||
product=product,
|
product=product,
|
||||||
quantity=qty,
|
quantity=quantity,
|
||||||
unit_price=price,
|
unit_price=price,
|
||||||
line_total=line_total
|
line_total=line_total
|
||||||
)
|
)
|
||||||
@ -1030,3 +1034,171 @@ def search_customers_api(request): return JsonResponse({'customers': []})
|
|||||||
def pos_sync_update(request): return JsonResponse({'success': False})
|
def pos_sync_update(request): return JsonResponse({'success': False})
|
||||||
@login_required
|
@login_required
|
||||||
def pos_sync_state(request): return JsonResponse({'success': False})
|
def pos_sync_state(request): return JsonResponse({'success': False})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def edit_purchase(request, pk):
|
||||||
|
purchase = get_object_or_404(Purchase, pk=pk)
|
||||||
|
suppliers = Supplier.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 purchase.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,
|
||||||
|
'cost_price': float(item.cost_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 = purchase.payments.first()
|
||||||
|
if first_payment and first_payment.payment_method:
|
||||||
|
payment_method_id = first_payment.payment_method.id
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'purchase': purchase,
|
||||||
|
'suppliers': suppliers,
|
||||||
|
'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/purchase_edit.html', context)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def update_purchase_api(request, pk):
|
||||||
|
if request.method != 'POST':
|
||||||
|
return JsonResponse({'success': False, 'error': 'Invalid request method'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
purchase = Purchase.objects.get(pk=pk)
|
||||||
|
data = json.loads(request.body)
|
||||||
|
|
||||||
|
supplier_id = data.get('supplier_id')
|
||||||
|
items = data.get('items', [])
|
||||||
|
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 purchase'})
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# 1. Revert Stock (Subtract what was added)
|
||||||
|
for item in purchase.items.all():
|
||||||
|
product = item.product
|
||||||
|
product.stock_quantity -= item.quantity
|
||||||
|
product.save()
|
||||||
|
|
||||||
|
# 2. Delete existing items
|
||||||
|
purchase.items.all().delete()
|
||||||
|
|
||||||
|
# 3. Update Purchase Details
|
||||||
|
if supplier_id:
|
||||||
|
purchase.supplier_id = supplier_id
|
||||||
|
else:
|
||||||
|
purchase.supplier = None
|
||||||
|
|
||||||
|
purchase.notes = notes
|
||||||
|
if invoice_number:
|
||||||
|
purchase.invoice_number = invoice_number
|
||||||
|
|
||||||
|
if due_date:
|
||||||
|
purchase.due_date = due_date
|
||||||
|
else:
|
||||||
|
purchase.due_date = None
|
||||||
|
|
||||||
|
# 4. Create New Items and Add Stock
|
||||||
|
total_amount = decimal.Decimal(0)
|
||||||
|
|
||||||
|
for item_data in items:
|
||||||
|
product = Product.objects.get(pk=item_data['id'])
|
||||||
|
quantity = decimal.Decimal(str(item_data['quantity']))
|
||||||
|
cost_price = decimal.Decimal(str(item_data['cost_price']))
|
||||||
|
|
||||||
|
# Add stock
|
||||||
|
product.stock_quantity += quantity
|
||||||
|
# Update product cost price (optional, but good practice to update to latest)
|
||||||
|
product.cost_price = cost_price
|
||||||
|
product.save()
|
||||||
|
|
||||||
|
line_total = cost_price * quantity
|
||||||
|
total_amount += line_total
|
||||||
|
|
||||||
|
PurchaseItem.objects.create(
|
||||||
|
purchase=purchase,
|
||||||
|
product=product,
|
||||||
|
quantity=quantity,
|
||||||
|
cost_price=cost_price,
|
||||||
|
line_total=line_total
|
||||||
|
)
|
||||||
|
|
||||||
|
purchase.total_amount = total_amount
|
||||||
|
|
||||||
|
# 5. Handle Payments
|
||||||
|
if payment_type == 'credit':
|
||||||
|
purchase.status = 'unpaid'
|
||||||
|
purchase.paid_amount = 0
|
||||||
|
purchase.balance_due = purchase.total_amount
|
||||||
|
purchase.payments.all().delete()
|
||||||
|
|
||||||
|
elif payment_type == 'cash':
|
||||||
|
purchase.status = 'paid'
|
||||||
|
purchase.paid_amount = purchase.total_amount
|
||||||
|
purchase.balance_due = 0
|
||||||
|
|
||||||
|
purchase.payments.all().delete()
|
||||||
|
PurchasePayment.objects.create(
|
||||||
|
purchase=purchase,
|
||||||
|
amount=purchase.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':
|
||||||
|
purchase.paid_amount = paid_amount
|
||||||
|
purchase.balance_due = purchase.total_amount - paid_amount
|
||||||
|
if purchase.balance_due <= 0:
|
||||||
|
purchase.status = 'paid'
|
||||||
|
purchase.balance_due = 0
|
||||||
|
else:
|
||||||
|
purchase.status = 'partial'
|
||||||
|
|
||||||
|
purchase.payments.all().delete()
|
||||||
|
PurchasePayment.objects.create(
|
||||||
|
purchase=purchase,
|
||||||
|
amount=paid_amount,
|
||||||
|
payment_method_id=payment_method_id if payment_method_id else None,
|
||||||
|
payment_date=timezone.now().date(),
|
||||||
|
notes='Partial Payment (Edit)'
|
||||||
|
)
|
||||||
|
|
||||||
|
purchase.save()
|
||||||
|
|
||||||
|
return JsonResponse({'success': True, 'purchase_id': purchase.id})
|
||||||
|
|
||||||
|
except Purchase.DoesNotExist:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Purchase not found'})
|
||||||
|
except Product.DoesNotExist:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Product not found'})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)})
|
||||||
@ -1,33 +1 @@
|
|||||||
import os
|
# This script has been disabled/removed as the project is deployed to the root.
|
||||||
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.")
|
|
||||||
@ -3,4 +3,5 @@ mysqlclient==2.2.7
|
|||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
pyzk==0.9
|
pyzk==0.9
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
whitenoise==6.6.0
|
whitenoise==6.6.0
|
||||||
|
requests
|
||||||
|
|||||||
1
test_write.txt
Normal file
1
test_write.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
modified content
|
||||||
Loading…
x
Reference in New Issue
Block a user