gozaresh anbar va eslahfactor

This commit is contained in:
javad committed 2026-06-26 10:42:07 +03:30
1 parent 739104115a
commit 7a305b8e19
7 files changed
+571 -62

No files matched your search

@@ -0,0 +1,18 @@
# Generated by Django 3.2.25 on 2026-06-25 15:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0046_auto_20260624_1818'),
]
operations = [
migrations.AlterField(
model_name='productinstore',
name='movement_type',
field=models.CharField(blank=True, choices=[('SALE', 'فروش عادی'), ('WARRANTY', 'گارانتی'), ('PROMOTION', 'اشانتیون'), ('TRANSFER', 'انتقال'), ('INITIAL', 'موجودی اولیه'), ('DIRECT_IN', 'ورود دستی'), ('DIRECT_OUT', 'خروج دستی'), ('RETURN', 'برگشت از فروش'), ('ERROR_CORRECTION', 'اصلاح خطای ثبت')], db_index=True, max_length=20, null=True),
),
]
+2
View File
@@ -155,6 +155,8 @@ class ProductInStore(models.Model):
INITIAL = 'INITIAL', 'موجودی اولیه'
DIRECT_IN = 'DIRECT_IN', 'ورود دستی'
DIRECT_OUT = 'DIRECT_OUT', 'خروج دستی'
RETURN = 'RETURN', 'برگشت از فروش'
ERROR_CORRECTION = 'ERROR_CORRECTION', 'اصلاح خطای ثبت'
StoreRoom = models.ForeignKey(StoreRoom, on_delete=models.CASCADE)
Product = models.ForeignKey(Product, on_delete=models.CASCADE)
@@ -95,6 +95,10 @@
گزارش
</a>
<a href="{% url 'admin_panel:inventory_movements_report' %}" style="padding-right: 10px;" class="navbar-item">
گزارش جامع انبار
</a>
</div>
</div>
@@ -4,35 +4,35 @@
{% block BodyCustomer1 %}
<div id="newFactor" class="column is-12-mobile is-12-desktop ">
<h1 class="has-text-centered has-text-weight-bold ">شماره فاکتور {{ factorO.id }}</h1>
<div class="columns is-multiline is-mobile">
<div id="newFactor" class="column is-12 p-2-mobile">
<h1 class="has-text-centered has-text-weight-bold is-size-4-mobile mb-4">شماره فاکتور {{ factorO.id }}</h1>
<div class="columns is-multiline">
<div class="field column is-4-desktop is-12-mobile m-0">
<label class="label">نام شخص</label>
<div class="field column is-4-desktop is-6-tablet is-12-mobile">
<label class="label is-small-mobile">نام شخص</label>
<div class="control">
<input disabled value="{{ customer.name_family }}" name="installDate" id="persianDateP" class="input" type="text" placeholder="مثل: 1400/06/01">
<input disabled value="{{ customer.name_family }}" name="installDate" id="persianDateP" class="input is-small-mobile" type="text">
</div>
</div>
<div class="field column is-4-desktop is-12-mobile m-0">
<label class="label">تاریخ فاکتور</label>
<div class="field column is-4-desktop is-6-tablet is-12-mobile">
<label class="label is-small-mobile">تاریخ فاکتور</label>
<div class="control">
<input id="FpayDate" value="{{ factorO.create_date | toJalali }}" id="persianDateP" class="input" type="text" placeholder="مثل: 1400/06/01">
<input disabled id="FpayDate" value="{{ factorO.create_date | toJalali }}" class="input is-small-mobile" type="text">
</div>
</div>
<div class="table-container column is-12-desktop is-12-mobile m-0">
<table class="table m-auto has-text-centered table is-bordered is-striped is-narrow is-fullwidth mojodiTB ">
<div class="table-container column is-12 p-0-mobile my-3">
<table class="table m-auto has-text-centered table is-bordered is-striped is-narrow is-fullwidth mojodiTB is-size-7-mobile">
<thead>
<tr>
<th style="min-width: 300px;">نام محصول</th>
<th style="min-width: 100px;">قیمت واحد</th>
<th style="min-width: 100px;">قیمت کل</th>
<th style="min-width: 200px;">نام محصول</th>
<th style="min-width: 90px;">قیمت واحد</th>
<th style="min-width: 90px;">قیمت کل</th>
<th>تعداد</th>
<th>بابت</th>
<th style="min-width: 50px;">عملیات</th>
<th style="min-width: 40px;">عملیات</th>
</tr>
</thead>
<tfoot>
@@ -49,82 +49,195 @@
{% for iteam in fiteam %}
<tr class="IteamPrd">
<td>
<span class="product_name">{{ iteam.product_name }}</span>
<span class="product_name">{{ iteam.product_name }}</span>
<span class="product_id is-hidden"></span>
</td>
<td class="product_price">{{ iteam.product.product_price }}</td>
<td class="product_price">{{ iteam.amount }}</td>
<td class="product_count" >{{ iteam.num }}</td>
<td class="product_count" >{{ iteam.garantyEsh }}</td>
<td>
<span>*</span>
</td>
<td class="product_count">{{ iteam.num }}</td>
<td class="product_count">{{ iteam.garantyEsh }}</td>
<td><span>*</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="field column is-4-desktop is-12-mobile m-0">
<label class="label">جمع فاکتور</label>
<div class="field column is-4-desktop is-6-tablet is-12-mobile">
<label class="label is-small-mobile">جمع فاکتور</label>
<div class="control">
<input value="{{ factorO.amount }}" disabled name="price" id="sum_all" class="input" type="text" placeholder="">
<input value="{{ factorO.amount }}" disabled name="price" id="sum_all" class="input is-small-mobile" type="text">
</div>
</div>
<div class="field column is-4-desktop is-12-mobile m-0">
<label class="label">نحوه پرداخت</label>
<div class="field column is-4-desktop is-6-tablet is-12-mobile">
<label class="label is-small-mobile">نحوه پرداخت</label>
<div class="control">
<div class="select" style="width: 100%;">
<select id="FpayType" required style="width: 100%;">
<option></option>
<option value="naghd">نقد</option>
<option value="cardToCard">کارت به کارت</option>
<option value="kartKhan">کارت خوان</option>
<option value="nesiye">نیسیه</option>
<option value="check">چک</option>
<div class="select is-small-mobile" style="width: 100%;">
<select id="FpayType" disabled style="width: 100%;">
<option value="{{ factorO.PayType }}">{{ factorO.PayType }}</option>
</select>
</div>
</div>
</div>
<div class="field column is-4-desktop is-12-mobile m-0" id="sarresidFild01">
<label class="label">سررسید</label>
<div class="field column is-4-desktop is-12-mobile" id="sarresidFild01">
<label class="label is-small-mobile">سررسید</label>
<div class="control">
<input value="{% if factorO.deadline %} {{ factorO.deadline | toJalali }} {% endif %}" name="sarResidDateSL" class="input" type="text" placeholder="">
<input value="{% if factorO.deadline %} {{ factorO.deadline | toJalali }} {% endif %}" disabled name="sarResidDateSL" class="input is-small-mobile" type="text">
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
<label class="label">توضیحات فاکتور فاکتور</label>
<div class="field column is-12">
<label class="label is-small-mobile">توضیحات فاکتور</label>
<div class="control">
<textarea id="Fdesc" class="textarea" placeholder="بابت؟">
<textarea id="Fdesc" disabled class="textarea is-small-mobile" rows="2">
{{ factorO.desc }}
</textarea></div>
</textarea>
</div>
</div>
<div class="field column is-12-desktop is-12-mobile m-0">
{% if perms.admin_panel.DelFactor %}
<span class="button is-danger" id="btnDelfactor09">حذف فاکتور و اصلاح انبار</span>
<div class="field column is-12 has-text-centered mt-4">
{% if factorO.is_active and factorO.pay_status != 'cancell' %}
{% if perms.admin_panel.DelFactor %}
<button class="button is-danger has-text-weight-bold is-fullwidth-mobile py-5" id="openCancelModal">
<i class="fas fa-exclamation-triangle m-l-5"></i> ابطال فاکتور و صدور اسناد اصلاحی
</button>
{% endif %}
{% else %}
<div class="notification is-danger is-light has-text-weight-bold is-size-6-mobile" style="border: 2px dashed #f14668;">
<i class="fas fa-info-circle"></i> این فاکتور باطل شده است. (اسناد مالی و انبار معکوس شده‌اند)
</div>
{% endif %}
</div>
</div>
</div>
<div class="modal" id="cancelFactorModal">
<div class="modal-background close-modal"></div>
<div class="modal-card mx-2-mobile" style="border-radius: 8px; max-width: 700px; width: auto;">
<header class="modal-card-head has-background-danger px-4-mobile py-3-mobile">
<p class="modal-card-title has-text-white has-text-weight-bold is-size-5 is-size-6-mobile">
<i class="fas fa-book-open"></i> راهنمای ابطال فاکتور {{ factorO.id }}
</p>
<button class="delete close-modal" aria-label="close"></button>
</header>
<section class="modal-card-body pb-5 px-3-mobile" style="direction: rtl; text-align: right;">
<div class="notification is-danger is-light mb-4 py-2 px-3 is-size-7 is-size-7-mobile">
<i class="fas fa-gavel"></i> <strong>قانون حذف فیزیکی:</strong> در سیستم‌های مالی چیزی حذف نمی‌شود؛ تایید این فرم مستقیماً اسناد معکوس در دفتر روزنامه و کاردکس انبار ثبت می‌کند. لطفاً با دقت انتخاب کنید.
</div>
<form id="cancelForm" method="POST" action="{% url 'admin_panel:FactorDel' c_id=customer.id f_id=factorO.id %}">
{% csrf_token %}
<div class="field mb-5">
<label class="label has-text-weight-bold has-text-link mb-2 is-size-6-mobile"><i class="fas fa-step-forward"></i> گام اول: ماهیت ابطال</label>
<div class="control">
<label class="radio is-block mb-3 p-3" style="background: #fdfdfd; border: 1px solid #dbdbdb; border-radius: 6px; cursor: pointer; white-space: normal;">
<input type="radio" name="cancel_reason" value="error" checked id="reasonError">
<strong class="has-text-info is-size-6 is-size-7-mobile mr-1">الف) اشتباه کاربری در ثبت فاکتور</strong>
<div class="is-size-7 has-text-grey mt-2 pr-4">
<p>• <strong>اثر انبار:</strong> کالا تحت عنوان "اصلاح خطای ثبت" بازمی‌گردد.</p>
<p class="mt-1">• <strong>اثر مالی:</strong> مبلغ فاکتور معکوس و <span class="has-text-danger has-text-weight-bold">تمامی پرداختی‌ها لغو می‌شوند.</span></p>
</div>
</label>
<label class="radio is-block p-3" style="background: #fdfdfd; border: 1px solid #dbdbdb; border-radius: 6px; cursor: pointer; white-space: normal;">
<input type="radio" name="cancel_reason" value="return" id="reasonReturn">
<strong class="has-text-warning-dark is-size-6 is-size-7-mobile mr-1">ب) برگشت از فروش واقعی (مرجوعی)</strong>
<div class="is-size-7 has-text-grey mt-2 pr-4">
<p>• <strong>اثر انبار:</strong> کالا رسماً تحت عنوان "برگشت از فروش" ثبت می‌شود.</p>
<p class="mt-1">• <strong>اثر مالی:</strong> مشتری بستانکار می‌شود. وضعیت وجه او در گام دوم مشخص می‌گردد.</p>
</div>
</label>
</div>
</div>
<div style="border-top: 1px dashed #ccc; margin: 15px 0;"></div>
<div class="field mb-5" id="paymentActionSection">
<label class="label has-text-weight-bold has-text-link mb-2 is-size-6-mobile"><i class="fas fa-step-forward"></i> گام دوم: وضعیت مبالغ مالی</label>
<div class="notification is-warning is-light p-3 is-size-7 mb-3" style="border-right: 4px solid #ffdd57;">
<i class="fas fa-exclamation-circle"></i> <strong>تله‌ی حسابداری:</strong> اگر پولی از مشتری گرفته نشده (یا عودت داده شده)، گزینه اول را انتخاب کنید تا از بستانکاری کاذب جلوگیری شود.
</div>
<div class="control">
<label class="radio is-block mb-2 p-2" style="background: #f9f9f9; border-radius: 4px; white-space: normal;">
<input type="radio" name="payment_action" value="reverse" checked id="payReverse">
<span class="is-size-6 is-size-7-mobile mr-1"><strong>سند معکوس پرداختی صادر شود</strong> <br><small class="has-text-grey">(پولی دریافت نشده بود / وجه عودت داده شد)</small></span>
</label>
<label class="radio is-block p-2" style="background: #f9f9f9; border-radius: 4px; white-space: normal;" id="payKeepWrapper">
<input type="radio" name="payment_action" value="keep" id="payKeep">
<span class="is-size-6 is-size-7-mobile mr-1"><strong>پول به عنوان اعتبار در حساب بماند</strong> <br><small class="has-text-grey">(مشتری وجه را پرداخت کرده و پس نمی‌گیرد)</small></span>
</label>
</div>
</div>
<div class="box has-background-danger-light mt-4 p-3" style="border: 1px solid #f14668;">
<label class="checkbox has-text-weight-semibold is-size-7 has-text-danger-dark" style="display: flex; align-items: flex-start;">
<input type="checkbox" id="understandCheckbox" class="ml-2 mt-1">
<span>اینجانب تایید می‌کنم که راهنما را مطالعه کرده‌ام و مسئولیت ثبت این سند اصلاحیِ انبار و مالی را می‌پذیرم.</span>
</label>
</div>
</form>
</section>
<footer class="modal-card-foot is-flex-wrap-wrap is-justify-content-flex-end p-3-mobile" style="background-color: #fafafa; direction: rtl;">
<button class="button is-danger is-fullwidth-mobile mb-2-mobile" id="submitCancelForm" disabled>تایید نهایی و صدور سند</button>
<button class="button close-modal is-fullwidth-mobile mr-2 mr-0-mobile">انصراف</button>
</footer>
</div>
</div>
<script>
$("#btnDelfactor09").click(function(){
var cDelok = confirm("باحذف فاکتور موجودی انبار نیز اصلاح میشود و تغییر می کند آیا مطمئن هستید؟")
if(cDelok){
window.location.href = "/panel/customer/{{ customer.id }}/factor/{{ factorO.id }}/del";
}
$(document).ready(function() {
$("#openCancelModal").click(function() {
$("#cancelFactorModal").addClass("is-active");
triggerFormLogic();
});
$(".close-modal").click(function() {
$("#cancelFactorModal").removeClass("is-active");
$("#understandCheckbox").prop('checked', false);
$("#submitCancelForm").prop('disabled', true);
});
$("#understandCheckbox").change(function() {
if($(this).is(":checked")) {
$("#submitCancelForm").prop('disabled', false);
} else {
$("#submitCancelForm").prop('disabled', true);
}
});
function triggerFormLogic() {
if($("#reasonError").is(':checked')) {
$("#payReverse").prop('checked', true);
$("#payKeep").prop('disabled', true);
$("#payKeepWrapper").css('opacity', '0.5');
} else {
$("#payKeep").prop('disabled', false);
$("#payKeepWrapper").css('opacity', '1');
}
}
$("input[name='cancel_reason']").change(function(){
triggerFormLogic();
});
$("#submitCancelForm").click(function(e) {
e.preventDefault();
$(this).addClass('is-loading').prop('disabled', true);
$(".close-modal").prop('disabled', true);
$("#payKeep").prop('disabled', false);
$("#cancelForm").submit();
});
});
</script>
{% endblock %}
@@ -0,0 +1,202 @@
{% extends "./base.html" %}
{% load custom_tags %}
{% block bodyPage %}
<div class="container is-fluid mt-4">
<div class="box">
<h1 class="title is-4 has-text-centered mb-5">گزارش تفکیکی گردش و خروج کالا از انبار</h1>
<form method="GET" class="columns is-multiline is-centered is-vcentered">
<div class="column is-2-desktop is-12-mobile">
<label class="label is-small">نوع تراکنش کالا</label>
<div class="select is-fullwidth is-small">
<select name="movement_type">
<option value="">-- همه موارد --</option>
{% for code, label in movement_choices %}
<option value="{{ code }}" {% if filters.movement_type == code %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-2-desktop is-12-mobile">
<label class="label is-small">کاربر صادرکننده</label>
<div class="select is-fullwidth is-small">
<select name="user_id">
<option value="">-- همه کاربران --</option>
{% for u in all_users %}
<option value="{{ u.id }}" {% if filters.user_id == u.id %}selected{% endif %}>
{{ u.first_name }} {{ u.last_name }} ({{ u.username }})
</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-2-desktop is-12-mobile">
<label class="label is-small">انبار</label>
<div class="select is-fullwidth is-small">
<select name="store_id">
<option value="">-- همه انبارها --</option>
{% for s in all_stores %}
<option value="{{ s.id }}" {% if filters.store_id == s.id %}selected{% endif %}>
{{ s.store_name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-2-desktop is-6-mobile">
<label class="label is-small">از تاریخ</label>
<input type="text" class="input is-small date-picker-az pwt-datepicker-input-element" placeholder="انتخاب کنید">
<input type="hidden" name="azTarikh" id="azTarikhAlt" value="{{ filters.azTarikh }}">
</div>
<div class="column is-2-desktop is-6-mobile">
<label class="label is-small">تا تاریخ</label>
<input type="text" class="input is-small date-picker-ta pwt-datepicker-input-element" placeholder="انتخاب کنید">
<input type="hidden" name="taTarikh" id="taTarikhAlt" value="{{ filters.taTarikh }}">
</div>
<div class="column is-2-desktop is-12-mobile mt-4">
<button type="submit" class="button is-danger is-small is-fullwidth">
<span>اعمال فیلتر</span>
</button>
</div>
</form>
</div>
<div class="columns is-multiline mb-3">
<div class="column is-6">
<div class="notification is-light is-danger p-3 has-text-centered">
<strong>جمع کل خروجی در این گزارش: </strong>
<span class="is-size-5 tag is-danger is-light">{{ summary_stats.total_out_qty|default:0 }} عدد</span>
</div>
</div>
<div class="column is-6">
<div class="notification is-light is-success p-3 has-text-centered">
<strong>جمع کل ورودی در این گزارش: </strong>
<span class="is-size-5 tag is-success is-light">{{ summary_stats.total_in_qty|default:0 }} عدد</span>
</div>
</div>
</div>
<div class="table-container box p-0">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth has-text-centered">
<thead>
<tr class="has-background-light">
<th>#</th>
<th>کد کالا</th>
<th>نام کالا</th>
<th>نوع تراکنش</th>
<th>خروج (-)</th>
<th>ورود (+)</th>
<th>موجودی لحظه‌ای</th>
<th>مشتری / گیرنده</th>
<th>کاربر ثبت‌کننده</th>
<th>انبار</th>
<th>تاریخ ثبت</th>
<th>توضیحات سند</th>
</tr>
</thead>
<tbody>
{% for item in page_obj %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ item.Product.id }}</td>
<td class="has-text-right"><strong>{{ item.Product.product_name }}</strong></td>
<td>
<span class="tag is-small
{% if item.movement_type == 'WARRANTY' %}is-warning
{% elif item.movement_type == 'PROMOTION' %}is-info
{% elif item.movement_type == 'SALE' %}is-success
{% else %}is-light{% endif %}">
{{ item.get_movement_type_display }}
</span>
</td>
<td class="has-text-danger has-text-weight-bold">{{ item.s_out|default:"-" }}</td>
<td class="has-text-success has-text-weight-bold">{{ item.s_in|default:"-" }}</td>
<td style="direction: ltr;">{{ item.current_stock_at_time }}</td>
<td>
{% if item.factor and item.factor.customer %}
<a href="{% url 'admin_panel:customer' item.factor.customer.id %}" target="_blank">
{{ item.factor.customer.name_family }}
</a>
{% else %}
<span class="has-text-grey-light">ثبت دستی/داخلی</span>
{% endif %}
</td>
<td>
{% if item.factor and item.factor.user %}
{{ item.factor.user.first_name }} {{ item.factor.user.last_name }}
{% else %}
سیستم / نامشخص
{% endif %}
</td>
<td>{{ item.StoreRoom.store_name }}</td>
<td><span class="is-size-7" style="direction: ltr; display: inline-block;">{{ item.createdDT|toJalali }}</span></td>
<td class="has-text-right is-size-7">{{ item.desc }}</td>
</tr>
{% empty %}
<tr>
<td colspan="12" class="py-5 has-text-grey">هیچ تراکنشی با این مشخصات یافت نشد.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.has_other_pages %}
<nav class="pagination is-centered is-small my-4" role="navigation" aria-label="pagination">
{% if page_obj.has_previous %}
<a class="pagination-previous" href="?page={{ page_obj.previous_page_number }}&movement_type={{ filters.movement_type }}&user_id={{ filters.user_id }}&store_id={{ filters.store_id }}&azTarikh={{ filters.azTarikh }}&taTarikh={{ filters.taTarikh }}">صفحه قبل</a>
{% endif %}
{% if page_obj.has_next %}
<a class="pagination-next" href="?page={{ page_obj.next_page_number }}&movement_type={{ filters.movement_type }}&user_id={{ filters.user_id }}&store_id={{ filters.store_id }}&azTarikh={{ filters.azTarikh }}&taTarikh={{ filters.taTarikh }}">صفحه بعد</a>
{% endif %}
<ul class="pagination-list">
<li><span class="pagination-link is-current">صفحه {{ page_obj.number }} از {{ page_obj.paginator.num_pages }}</span></li>
</ul>
</nav>
{% endif %}
</div>
{% endblock %}
{% block jsEnd %}
<script>
$(document).ready(function() {
// تنظیم تقویم شمسی برای تاریخ شروع
var pickerAz = $(".date-picker-az").pDatepicker({
altField: '#azTarikhAlt',
initialValue: false,
autoClose: true,
format: 'YYYY/MM/DD',
calendar: { persian: { locale: 'fa' } }
});
// تنظیم تقویم شمسی برای تاریخ پایان
var pickerTa = $(".date-picker-ta").pDatepicker({
altField: '#taTarikhAlt',
initialValue: false,
autoClose: true,
format: 'YYYY/MM/DD',
calendar: { persian: { locale: 'fa' } }
});
// لود کردن تاریخ‌های قبلی در صورت فیلتر بودن صفحه
{% if filters.azTarikh %}
pickerAz.setDate({{ filters.azTarikh }});
{% endif %}
{% if filters.taTarikh %}
pickerTa.setDate({{ filters.taTarikh }});
{% endif %}
});
</script>
{% endblock %}
+1
View File
@@ -11,6 +11,7 @@ urlpatterns = [
path('p_logout/', views.p_logout, name='p_logout'),
path('bedehi/', views.bedehi, name='bedehi'),
path('report/', views.report, name='report'),
path('report/inventory-movements/', views.inventory_movements_report, name='inventory_movements_report'),
path('hesabdari/', views.hesabdari, name='hesabdari'),
path('hesabdari/sarresid_nashode/', views.hesabdari_residnashone, name='hesabdari_residnashone'),
path('hesabdari/check/pass/', views.hesabdari_checkPass, name='hesabdari_check_pass'),
+175 -6
View File
@@ -8,6 +8,7 @@ from django.utils import timezone # جدید
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.core.exceptions import ValidationError # جدید
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest # بخشBadRequest جدید
from django.db import transaction, connection
from django.db.models import Count, Sum, Q
@@ -183,7 +184,83 @@ def report(request):
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
return render(request, 'admin_panel/report.html', data)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def inventory_movements_report(request):
"""
گزارش جامع گردش و خروج کالا از انبار به تفکیک نوع تراکنش (گارانتی، اشانتیون، فروش و...)،
کاربر ثبت‌کننده، انبار و بازه زمانی شمسی.
"""
# واکشی اطلاعات اولیه برای 드롭‌داون‌های فیلتر در قالب
all_users = User.objects.all().order_by('first_name')
all_stores = StoreRoom.objects.all()
movement_choices = ProductInStore.MovementType.choices
# دریافت پارامترهای فیلتر از فرانت‌اند
selected_type = request.GET.get('movement_type', '')
selected_user = request.GET.get('user_id', '')
selected_store = request.GET.get('store_id', '')
az_tarikh = request.GET.get('azTarikh', '')
ta_tarikh = request.GET.get('taTarikh', '')
# ۱. کوئری پایه: اعمال select_related جهت جلوگیری از فاجعه N+1 در پایگاه داده
movements_list = ProductInStore.objects.select_related(
'Product', 'StoreRoom', 'factor__customer', 'factor__user'
).filter(year=1400).order_by('-createdDT')
# ۲. اعمال فیلترهای پویای کاربر
if selected_type:
movements_list = movements_list.filter(movement_type=selected_type)
if selected_user:
# فیلتر بر اساس کاربری که فاکتور خروج کالا را در سیستم ثبت کرده است
movements_list = movements_list.filter(factor__user__id=_safe_int(selected_user))
if selected_store:
movements_list = movements_list.filter(StoreRoom__id=_safe_int(selected_store))
# ۳. پردازش بازه زمانی (تبدیل تایم‌استمپ جاوااسکریپت به تاریخ میلادی)
if az_tarikh:
try:
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
movements_list = movements_list.filter(createdDT__gte=az_dt.strftime('%Y-%m-%d 00:00:00'))
except (ValueError, TypeError):
pass
if ta_tarikh:
try:
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
movements_list = movements_list.filter(createdDT__lte=ta_dt.strftime('%Y-%m-%d 23:59:59'))
except (ValueError, TypeError):
pass
# ۴. محاسبه آماری سریع روی نتایج فیلتر شده (بدون نیاز به پایش دستی در پایتون)
summary_stats = movements_list.aggregate(
total_out_qty=Sum('s_out'),
total_in_qty=Sum('s_in')
)
# ۵. صفحه‌بندی نتایج (۵۰ رکورد در هر صفحه جهت حفظ سرعت لود مرورگر)
paginator = Paginator(movements_list, 50)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'all_users': all_users,
'all_stores': all_stores,
'movement_choices': movement_choices,
'summary_stats': summary_stats,
'filters': {
'movement_type': selected_type,
'user_id': _safe_int(selected_user) if selected_user else '',
'store_id': _safe_int(selected_store) if selected_store else '',
'azTarikh': az_tarikh,
'taTarikh': ta_tarikh,
}
}
return render(request, 'admin_panel/report_inventory_movements.html', context)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
@@ -1215,13 +1292,105 @@ def newFactor2(request, c_id):
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.DelFactor', raise_exception=True)
def FactorDel(request, c_id ,f_id):
@require_POST
def FactorDel(request, c_id, f_id):
customer_obj = get_object_or_404(Customer, id=c_id)
factor_initial = get_object_or_404(Factor, id=f_id, customer=customer_obj)
if factor_initial.pay_status == 'cancell' or not factor_initial.is_active:
return HttpResponseBadRequest("این فاکتور قبلاً باطل یا برگشت داده شده است.")
user_fullname = f"{request.user.first_name} {request.user.last_name}".strip() or request.user.username
cancel_reason = request.POST.get('cancel_reason', 'error')
payment_action = request.POST.get('payment_action', 'keep')
if cancel_reason == 'return':
reason_text = "برگشت از فروش (مرجوعی مشتری)"
inv_movement = ProductInStore.MovementType.RETURN
else:
reason_text = "اصلاح خطای کاربری"
inv_movement = ProductInStore.MovementType.ERROR_CORRECTION
try:
#ProductInStore.objects.filter(factor__id= f_id).delete()
Factor.objects.get(id=f_id).delete()
return redirect('/panel/customer/'+str(c_id)+'/factors/')
except:
return HttpResponse("خطا در حذف فاکتور")
with transaction.atomic():
if connection.features.has_select_for_update:
factor = Factor.objects.select_for_update().get(pk=factor_initial.pk)
else:
factor = Factor.objects.get(pk=factor_initial.pk)
if factor.pay_status == 'cancell' or not factor.is_active:
return HttpResponseBadRequest("خطای همزمانی: این فاکتور در لحظه فرستادن درخواست باطل شده بود.")
factor.is_active = False
factor.pay_status = 'cancell'
factor.save(update_fields=['is_active', 'pay_status'])
# واکشی اسناد پرداخت قبل از ایجاد سند جدید (جلوگیری از باگ تکرار سند)
payments_to_reverse = []
if payment_action == 'reverse' or cancel_reason == 'error':
payments_to_reverse = list(Article.objects.filter(
Q(debit=0) | Q(debit__isnull=True),
factor=factor,
credit__gt=0
))
# ۱. صدور سند معکوس مالی برای کل مبلغ فاکتور
factor_amount = int(factor.amount or 0)
if factor_amount > 0:
Article.objects.create(
debit=0,
credit=factor_amount,
customer=customer_obj,
factor=factor,
type=factor.PayType,
desc=f"برگشت از فروش و ابطال فاکتور شماره {factor.id} (مرجوعی کالا توسط مشتری) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال فاکتور اشتباه شماره {factor.id} (بدون جابجایی واقعی کالا) - کاربر: {user_fullname}",
user=request.user,
payStatus="true"
)
# ۲. خنثی کردن پرداخت‌های اولیه فاکتور
for p in payments_to_reverse:
Article.objects.create(
debit=p.credit,
credit=0,
customer=customer_obj,
factor=factor,
type=p.type,
nahvePardakht=p.nahvePardakht,
desc=f"عودت وجه پرداخت شده / ابطال رسید فاکتور {factor.id} (به دلیل مرجوعی کالا) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال رسید پرداخت فاکتور اشتباه شماره {factor.id} - کاربر: {user_fullname}",
user=request.user,
payStatus="true"
)
# ۳. 🛠️ اصلاح انبار (حل باگ عدم تغییر موجودی سیستم)
stock_movements = list(ProductInStore.objects.filter(factor=factor, s_out__gt=0))
for movement in stock_movements:
pInStore = ProductInStore(
StoreRoom=movement.StoreRoom,
Product=movement.Product,
factor=factor,
s_in=movement.s_out, # برگرداندن تعداد کسر شده به ستون ورودی
s_out=0,
year=movement.year, # 💡 فیکس قطعی: استفاده دقیق از سال مالی تراکنش اصلی (۱۴۰۰) جهت همخوانی با توابع سیستم
desc=f"ورود به انبار بابت برگشت از فروش کالا (فاکتور {factor.id}) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"ورود به انبار بابت اصلاح خطای ثبت فاکتور {factor.id} - کاربر: {user_fullname}",
movement_type=inv_movement
)
pInStore.save() # فراخوانی متد save مدل شما که محاسبات تاریخچه و موجودی لحظه‌ای را انجام می‌دهد
# ۴. ابطال اقساط و سررسیدهای پاس نشده
Sarresid.objects.filter(factor=factor, payStatus="false").update(
payStatus="cancell",
desc=f"ابطال بابت {reason_text} فاکتور {factor.id}"
)
logger.info(f"Factor {factor.id} successfully cancelled. Reason: {cancel_reason}, Payment: {payment_action}")
except Exception as e:
logger.error(f"Critical error in FactorDel for factor {f_id}: {str(e)}", exc_info=True)
return HttpResponse("خطای سیستمی در فرآیند ابطال و صدور اسناد اصلاحی رخ داد.", status=500)
return redirect('admin_panel:customerFactors', c_id=c_id)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.DelCustomer', raise_exception=True)