gozaresh anbar va eslahfactor
This commit is contained in:
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),
|
||||
),
|
||||
]
|
||||
@@ -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 %}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user